diff --git a/.env.ci b/.env.ci new file mode 100644 index 00000000..b10a6d3e --- /dev/null +++ b/.env.ci @@ -0,0 +1,27 @@ +# Environment overrides for docker-compose.ci.yml. +# Hosts here point to compose service names, NOT localhost. +# Secrets (GOOGLE_*, MAIL_*, BETTER_AUTH_SECRET) are injected from GitHub +# Secrets in the workflow — they are intentionally NOT committed here. + +APP_NAME=Intra +APP_DOMAIN=intra.com +APP_SUPPORT_EMAIL=intra.feedback360.service@gmail.com +APP_PORT=8080 +APP_HOST=api +APP_PROTOCOL=http +APP_FRONTEND_PORT=3000 +APP_FRONTEND_HOST=web +APP_FRONTEND_PROTOCOL=http +APP_NODE_ENV=test +# Browser-visible URL — Cypress hits the host network, not the docker network. +NEXT_PUBLIC_API_URL=http://localhost:8080 + +DATABASE_HOST=postgres +DATABASE_PORT=5432 +DATABASE_USER=root +DATABASE_PASSWORD=rootpassword +DATABASE_NAME=intra_test +DATABASE_URL=postgresql://root:rootpassword@postgres:5432/intra_test + +MAIL_FROM=Intra 360° Feedback Service +BETTER_AUTH_URL=http://localhost:8080 diff --git a/.env.test.example b/.env.test.example index 0d487f92..b1d6835d 100644 --- a/.env.test.example +++ b/.env.test.example @@ -37,3 +37,7 @@ MAIL_FROM='Intra 360° Feedback Service ' # Auth Configuration BETTER_AUTH_URL="${APP_PROTOCOL}://${APP_HOST}:${APP_PORT}" BETTER_AUTH_SECRET='secret' + +# Cypress Configuration +CYPRESS_PROJECT_ID='project_id' +CYPRESS_RECORD_KEY='key' diff --git a/.env.test.local.example b/.env.test.local.example new file mode 100644 index 00000000..c8ef65e6 --- /dev/null +++ b/.env.test.local.example @@ -0,0 +1,15 @@ +# Local-only secrets that the `.gitignore` keeps out of the repo. +# Copy this file to `.env.test.local` and fill in the real values to enable +# `--record` runs against Cypress Cloud from your machine. +# +# cp .env.test.local.example .env.test.local +# $EDITOR .env.test.local +# +# `.gitignore` already covers `.env.test.local` and any `.env.*.local`, so +# nothing here ever reaches the remote. + +# Cypress Cloud — public id of the project on cloud.cypress.io +CYPRESS_PROJECT_ID= + +# Cypress Cloud — secret record key. NEVER commit, NEVER paste in chat. +CYPRESS_RECORD_KEY= diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml new file mode 100644 index 00000000..487cbc95 --- /dev/null +++ b/.github/workflows/cypress.yml @@ -0,0 +1,33 @@ +name: Cypress Tests +on: [push] +jobs: + cypress-run: + runs-on: ubuntu-latest + # Runs tests in parallel with matrix strategy https://on.cypress.io/parallelization + # https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/run-job-variations + # Also see warning here https://github.com/cypress-io/github-action#parallel + strategy: + fail-fast: false # https://github.com/cypress-io/github-action/issues/48 + matrix: + containers: [1, 2] # Uses 2 parallel instances + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Cypress run + # Uses the official Cypress GitHub action https://github.com/cypress-io/github-action + uses: cypress-io/github-action@v7 + with: + # Starts web server for E2E tests - replace with your own server invocation + # https://docs.cypress.io/app/continuous-integration/overview#Boot-your-server + start: npm start + wait-on: 'http://localhost:3000' # Waits for above + # Records to Cypress Cloud + # https://docs.cypress.io/cloud/get-started/setup + record: true + parallel: true # Runs test in parallel using settings above + env: + # For recording and parallelization you must set your CYPRESS_RECORD_KEY + # in GitHub repo → Settings → Secrets → Actions + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + # Create a token https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..43a14d17 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,98 @@ +name: E2E Usability Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: {} + +# Avoid stacked runs on the same branch. +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true + +jobs: + cypress: + name: Cypress (record to Cloud) + runs-on: ubuntu-latest + timeout-minutes: 30 + + env: + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile + env: + # Cypress binary is downloaded by the e2e step below. + CYPRESS_INSTALL_BINARY: 0 + + - name: Build docker images and start stack + run: | + docker compose \ + -f docker-compose.yml \ + -f docker-compose.ci.yml \ + --env-file .env.ci \ + up -d --build + + - name: Wait for stack readiness + run: | + pnpm dlx wait-on \ + http://localhost:8080/api/health \ + http://localhost:3000 \ + --timeout 180000 + + - name: Seed test database + run: pnpm db:test:refresh + + - name: Run Cypress (record to Cloud) + uses: cypress-io/github-action@v6 + with: + install: false + working-directory: apps/api/test/web-e2e + browser: chrome + record: true + config-file: cypress.config.ts + env: + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + # Browser-visible URL (cypress run from the GH runner, not from docker). + NEXT_PUBLIC_API_URL: http://localhost:8080 + + - name: Upload metrics artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: usability-metrics + path: apps/api/test/web-e2e/metrics.json + if-no-files-found: ignore + + - name: Upload Cypress screenshots (on failure) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: cypress-screenshots + path: apps/api/test/web-e2e/cypress/screenshots + if-no-files-found: ignore + + - name: Tear down docker stack + if: always() + run: | + docker compose \ + -f docker-compose.yml \ + -f docker-compose.ci.yml \ + down -v --remove-orphans diff --git a/.gitignore b/.gitignore index 139c6b8a..7c4d8d4d 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,9 @@ src/generated tsconfig.tsbuildinfo .eslintcache +.vscode +.postman + +# --- k6 load test results --- +apps/api/test/load/results/* +!apps/api/test/load/results/.gitkeep diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6a1ec891..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - // "files.exclude": { - // "apps/api/node_modules/@intra/database": true - // }, - // "search.exclude": { - // "**/apps/api/node_modules/@intra/database/**": true - // } - // "[prisma]": { - // "editor.defaultFormatter": "Prisma.prisma", - // "editor.formatOnSave": true - // } -} \ No newline at end of file diff --git a/Dockerfile.ci b/Dockerfile.ci new file mode 100644 index 00000000..5e0245bc --- /dev/null +++ b/Dockerfile.ci @@ -0,0 +1,92 @@ +# syntax=docker/dockerfile:1.7 +# +# Multi-stage build for CI usability tests. +# +# Two runnable targets are exposed: +# - api-runtime → NestJS server (port 8080, NODE_ENV=test) +# - web-runtime → Next.js server (port 3000, NODE_ENV=test) +# +# Both share the same dependency-install + workspace-build base, so the image +# layer cache is reused across services. + +# ────────────────────────────────────────────────────────────────────────────── +# 0. base — node + pnpm + native deps prisma needs on Alpine +# ────────────────────────────────────────────────────────────────────────────── +FROM node:22-alpine AS base +ENV PNPM_HOME="/pnpm" \ + PATH="/pnpm:$PATH" \ + CI=true +RUN corepack enable && apk add --no-cache openssl libc6-compat +WORKDIR /app + +# ────────────────────────────────────────────────────────────────────────────── +# 1. deps — install workspace dependencies (cacheable) +# ────────────────────────────────────────────────────────────────────────────── +FROM base AS deps +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json .npmrc ./ +COPY apps/api/package.json apps/api/package.json +COPY apps/web/package.json apps/web/package.json +COPY apps/api/test/web-e2e/package.json apps/api/test/web-e2e/package.json +COPY packages/database/package.json packages/database/package.json +COPY packages/shared-kernel/package.json packages/shared-kernel/package.json +# Cypress binary is irrelevant for runtime images — skip its postinstall. +ENV CYPRESS_INSTALL_BINARY=0 +RUN pnpm install --frozen-lockfile + +# ────────────────────────────────────────────────────────────────────────────── +# 2. workspace-build — TS build for shared-kernel + database (prisma generate) +# ────────────────────────────────────────────────────────────────────────────── +FROM deps AS workspace-build +COPY tsconfig.json tsconfig.build.json ./ +COPY packages packages +RUN pnpm --filter @intra/shared-kernel build +# `database build` runs `tsc` + `prisma generate` against .env.test +COPY .env.test ./.env.test +RUN pnpm --filter @intra/database build + +# ────────────────────────────────────────────────────────────────────────────── +# 3a. api-build — NestJS production bundle +# ────────────────────────────────────────────────────────────────────────────── +FROM workspace-build AS api-build +COPY apps/api apps/api +RUN pnpm --filter @intra/api build + +# ────────────────────────────────────────────────────────────────────────────── +# 3b. web-build — Next.js production bundle (NODE_ENV=production for `next build`) +# ────────────────────────────────────────────────────────────────────────────── +FROM workspace-build AS web-build +COPY apps/web apps/web +ENV NODE_ENV=production +RUN pnpm --filter @intra/web build + +# ────────────────────────────────────────────────────────────────────────────── +# 4a. api-runtime — minimal image for `node dist/main.js` +# ────────────────────────────────────────────────────────────────────────────── +FROM base AS api-runtime +ENV NODE_ENV=test +COPY --from=api-build /app/node_modules /app/node_modules +COPY --from=api-build /app/packages /app/packages +COPY --from=api-build /app/apps/api/dist /app/apps/api/dist +COPY --from=api-build /app/apps/api/package.json /app/apps/api/package.json +COPY --from=api-build /app/apps/api/node_modules /app/apps/api/node_modules +COPY --from=api-build /app/pnpm-workspace.yaml /app/package.json /app/ +EXPOSE 8080 +WORKDIR /app/apps/api +CMD ["node", "dist/main.js"] + +# ────────────────────────────────────────────────────────────────────────────── +# 4b. web-runtime — `next start` (production server) on 3000 +# ────────────────────────────────────────────────────────────────────────────── +FROM base AS web-runtime +ENV NODE_ENV=production +COPY --from=web-build /app/node_modules /app/node_modules +COPY --from=web-build /app/packages /app/packages +COPY --from=web-build /app/apps/web/.next /app/apps/web/.next +COPY --from=web-build /app/apps/web/public /app/apps/web/public +COPY --from=web-build /app/apps/web/package.json /app/apps/web/package.json +COPY --from=web-build /app/apps/web/node_modules /app/apps/web/node_modules +COPY --from=web-build /app/apps/web/next.config.ts /app/apps/web/next.config.ts +COPY --from=web-build /app/pnpm-workspace.yaml /app/package.json /app/ +EXPOSE 3000 +WORKDIR /app/apps/web +CMD ["node_modules/.bin/next", "start", "-p", "3000"] diff --git a/README.md b/README.md index d3fc6dbe..9c5aaad5 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ intra/ ├── package.json # Root scripts (Turbo entrypoints) + shared dev tooling ├── tsconfig.json # Base TS config inherited by every package ├── eslint.config.mjs # Shared ESLint base -├── LICENSE # MIT +├── LICENSE └── README.md # You are here ``` @@ -469,4 +469,4 @@ Each workspace has a focused README that goes deep on its own architecture and s ## License -[MIT](LICENSE) © Inessa Repeshko, 2026 +[LICENSE](LICENSE) © Inessa Repeshko, 2026 diff --git a/apps/api/package.json b/apps/api/package.json index 9c910fcd..d6605678 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "description": "Intra REST API", "private": true, - "license": "MIT", "author": "Inessa Repeshko", "scripts": { "// Environment variables": "", @@ -27,12 +26,16 @@ "lint": "eslint \"{src,test}/**/*.ts\" --fix", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "// Testing Commands": "", - "test": "pnpm jest --clearCache && pnpm jest", - "test:unit": "pnpm jest --watch --testMatch \"**/*.spec.ts\"", - "test:e2e": "pnpm jest --watch --testMatch \"**/*.e2e-spec.ts\"", - "test:watch": "pnpm jest --watch", - "test:cov": "pnpm jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" + "test": "pnpm jest --clearCache && pnpm test:unit", + "test:unit": "pnpm jest --config test/unit/jest.unit.config.ts", + "test:unit:cov": "pnpm jest --config test/unit/jest.unit.config.ts --coverage", + "test:unit:watch": "pnpm jest --config test/unit/jest.unit.config.ts --watch", + "test:unit:dashboard": "JEST_RESULTS_OUTPUT=test/unit/results.json pnpm jest --config test/unit/jest.unit.config.ts --reporters=default --reporters=./test/scripts/json-results.reporter.js; tsx test/scripts/generate-test-dashboard.ts test/unit/results.json test/unit/dashboard.html 'Unit suite'", + "test:integ": "pnpm env:test -- pnpm jest --config test/integration/jest.integration.config.ts", + "test:integ:cov": "pnpm env:test -- pnpm jest --config test/integration/jest.integration.config.ts --coverage", + "test:integ:dashboard": "JEST_RESULTS_OUTPUT=test/integration/results.json pnpm env:test -- pnpm jest --config test/integration/jest.integration.config.ts --reporters=default --reporters=./test/scripts/json-results.reporter.js; tsx test/scripts/generate-test-dashboard.ts test/integration/results.json test/integration/dashboard.html 'Integration suite'", + "test:integ:refresh": "pnpm --filter @intra/database db:test:refresh", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand --config test/unit/jest.unit.config.ts" }, "dependencies": { "@intra/database": "workspace:*", diff --git a/apps/api/src/contexts/feedback360/application/services/review-scheduler.service.ts b/apps/api/src/contexts/feedback360/application/services/review-scheduler.service.ts index 23242120..ae351c06 100644 --- a/apps/api/src/contexts/feedback360/application/services/review-scheduler.service.ts +++ b/apps/api/src/contexts/feedback360/application/services/review-scheduler.service.ts @@ -1,13 +1,35 @@ -import { CycleStage, ReviewStage, SYSTEM_ACTOR } from '@intra/shared-kernel'; +import { + CycleStage, + IdentityRole, + ReviewStage, + SYSTEM_ACTOR, +} from '@intra/shared-kernel'; import { Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; import { TRANSITION_REASONS } from '../constants/review-stage-transitions'; import { CycleService } from './cycle.service'; import { ReviewService } from './review.service'; /** - * Scheduled service for checking review deadlines - * Runs periodically to transition reviews to PREPARING_REPORT when deadline passes + * Synthetic actor used by the scheduler when calling access-checked + * methods on `ReviewService` + */ +const SYSTEM_USER = UserDomain.create({ + id: SYSTEM_ACTOR.ID, + firstName: SYSTEM_ACTOR.NAME, + lastName: '', + email: 'system@intra.local', + roles: [IdentityRole.ADMIN], +}); + +/** + * Scheduled service for checking review deadlines. + * Runs periodically to transition reviews to FINISHED once their cycle's + * `responseDeadline` has passed. The review state machine only allows + * IN_PROGRESS → FINISHED | CANCELED — moving directly to PREPARING_REPORT + * is not a valid transition, so the deadline trigger lands the review in + * FINISHED and the report-generation flow takes it from there. */ @Injectable() export class ReviewSchedulerService { @@ -28,7 +50,9 @@ export class ReviewSchedulerService { try { // Get all active cycles - const cycles = await this.cycleService.search({ isActive: true }); + const cycles = await this.cycleService.search({ + isActive: true, + }); for (const cycle of cycles) { if (!cycle.responseDeadline) { @@ -40,28 +64,31 @@ export class ReviewSchedulerService { // If deadline has passed if (now > deadline) { - // Find all reviews in this cycle that are still IN_PROGRESS - const reviews = await this.reviewService.search({ - cycleId: cycle.id!, - stage: ReviewStage.IN_PROGRESS, - }); + // Find all reviews in this cycle that are still IN_PROGRESS. + const reviews = await this.reviewService.search( + { + cycleId: cycle.id!, + stage: ReviewStage.IN_PROGRESS, + }, + SYSTEM_USER, + ); this.logger.log( `Found ${reviews.length} reviews past deadline in cycle #${cycle.id}`, ); - // Transition each review to PREPARING_REPORT + // Transition each review to FINISHED for (const review of reviews) { try { await this.reviewService.changeReviewStage( review.id!, - ReviewStage.PREPARING_REPORT, + ReviewStage.FINISHED, SYSTEM_ACTOR.ID, SYSTEM_ACTOR.NAME, TRANSITION_REASONS.DEADLINE_REACHED, ); this.logger.log( - `Transitioned review #${review.id} to PREPARING_REPORT (deadline)`, + `Transitioned review #${review.id} to FINISHED (deadline)`, ); } catch (error) { this.logger.error( diff --git a/apps/api/src/database/prisma.service.ts b/apps/api/src/database/prisma.service.ts index 07affac8..e2bcefec 100644 --- a/apps/api/src/database/prisma.service.ts +++ b/apps/api/src/database/prisma.service.ts @@ -15,12 +15,9 @@ export class PrismaService implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(PrismaService.name); + private readonly pool: Pool; constructor(private readonly configService: ConfigService) { - // const adapter = new PrismaPg({ - // connectionString: configService.get('database.url'), - // }); - const isProduction = configService.get('APP_NODE_ENV') === 'production'; const poolConfig: PoolConfig = { @@ -32,6 +29,7 @@ export class PrismaService const pool = new Pool(poolConfig); const adapter = new PrismaPg(pool); super({ adapter }); + this.pool = pool; } async onModuleInit() { @@ -53,6 +51,7 @@ export class PrismaService async onModuleDestroy() { this.logger.log('Disconnecting from database...'); await this.$disconnect(); + await this.pool.end(); this.logger.log('Disconnected from database'); } } diff --git a/apps/api/test/app/app.controller.spec.ts b/apps/api/test/app/app.controller.spec.ts deleted file mode 100644 index 7e35adbc..00000000 --- a/apps/api/test/app/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from '../../src/app.controller'; -import { AppService } from '../../src/app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/apps/api/test/feedback360/feedback360.controller.spec.ts b/apps/api/test/feedback360/feedback360.controller.spec.ts deleted file mode 100644 index 8b11c372..00000000 --- a/apps/api/test/feedback360/feedback360.controller.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { Feedback360Service } from '../../src/modules/feedback360/application/feedback360.service'; -import { Feedback360Repository } from '../../src/modules/feedback360/infrastructure/prisma-repositories/feedback360.repository'; -import { Feedback360Controller } from '../../src/modules/feedback360/presentation/http/controllers/feedback360.controller'; - -describe('Feedback360Controller', () => { - let controller: Feedback360Controller; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [Feedback360Controller], - providers: [ - Feedback360Service, - { - provide: Feedback360Repository, - useValue: { - create: jest.fn(), - findAll: jest.fn(), - findById: jest.fn(), - findByRateeId: jest.fn(), - findByHrId: jest.fn(), - findByPositionId: jest.fn(), - findByCycleId: jest.fn(), - findByReportId: jest.fn(), - findByStage: jest.fn(), - updateById: jest.fn(), - deleteById: jest.fn(), - }, - }, - ], - }).compile(); - - controller = module.get(Feedback360Controller); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/api/test/feedback360/feedback360.service.spec.ts b/apps/api/test/feedback360/feedback360.service.spec.ts deleted file mode 100644 index 4014f763..00000000 --- a/apps/api/test/feedback360/feedback360.service.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { Feedback360Service } from '../../src/modules/feedback360/application/feedback360.service'; -import { Feedback360Repository } from '../../src/modules/feedback360/infrastructure/prisma-repositories/feedback360.repository'; - -describe('Feedback360Service', () => { - let service: Feedback360Service; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - Feedback360Service, - { - provide: Feedback360Repository, - useValue: { - create: jest.fn(), - findAll: jest.fn(), - findById: jest.fn(), - findByRateeId: jest.fn(), - findByHrId: jest.fn(), - findByPositionId: jest.fn(), - findByCycleId: jest.fn(), - findByReportId: jest.fn(), - findByStage: jest.fn(), - updateById: jest.fn(), - deleteById: jest.fn(), - }, - }, - ], - }).compile(); - - service = module.get(Feedback360Service); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/api/test/integration/contexts/feedback360/listeners/cycle-stage.listener.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/listeners/cycle-stage.listener.integration-spec.ts new file mode 100644 index 00000000..002db3df --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/listeners/cycle-stage.listener.integration-spec.ts @@ -0,0 +1,201 @@ +import '../../../setup-env'; + +import { CycleStage, ReviewStage } from '@intra/shared-kernel'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TestingModule } from '@nestjs/testing'; +import { CycleStageChangedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-changed.event'; +import { CycleStageListener } from 'src/contexts/feedback360/application/listeners/cycle-stage.listener'; +import { ClusterScoreAnalyticsService } from 'src/contexts/feedback360/application/services/cluster-score-analytics.service'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { ClusterRepository } from 'src/contexts/library/infrastructure/prisma-repositories/cluster.repository'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('CycleStageListener (feedback360, integration)', () => { + let module: TestingModule; + let listener: CycleStageListener; + let analytics: ClusterScoreAnalyticsService; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let competenceRepo: CompetenceRepository; + let clusterRepo: ClusterRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let eventEmitter: EventEmitter2; + let prisma: PrismaService; + + let cycleId: number; + let clusterId: number; + let rateeId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + listener = module.get(CycleStageListener); + analytics = module.get(ClusterScoreAnalyticsService); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + competenceRepo = module.get(CompetenceRepository); + clusterRepo = module.get(ClusterRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + eventEmitter = module.get(EventEmitter2); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const competence = await competenceRepo.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const cluster = await clusterRepo.create( + ClusterDomain.create({ + competenceId: competence.id!, + lowerBound: 0, + upperBound: 5, + title: 'Beginner', + description: '', + }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + + cycleId = cycle.id!; + clusterId = cluster.id!; + rateeId = ratee.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function seedClusterScore(reviewId: number) { + await prisma.clusterScore.create({ + data: { + cycleId, + clusterId, + rateeId, + reviewId, + score: '3.5000', + answersCount: 4, + }, + }); + } + + async function seedReview(): Promise { + const hr = await identityUsers.create({ + firstName: 'HR', + lastName: 'P', + email: `hr2.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = (await positionRepo.search({} as any))[0]; + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'HR P', + cycleId, + stage: ReviewStage.FINISHED, + }), + ); + return review.id!; + } + + describe('handleCycleStageChanged', () => { + it('emits cycle.stage.processed and does nothing else when toStage is not FINISHED', async () => { + const processedEvents: any[] = []; + eventEmitter.on('cycle.stage.processed', (e) => + processedEvents.push(e), + ); + + await listener.handleCycleStageChanged( + new CycleStageChangedEvent( + cycleId, + CycleStage.NEW, + CycleStage.ACTIVE, + ), + ); + + expect(processedEvents).toHaveLength(1); + expect(processedEvents[0].cycleId).toBe(cycleId); + expect(processedEvents[0].currentStage).toBe(CycleStage.ACTIVE); + + const analyticsRows = await analytics.getByCycleId(cycleId); + expect(analyticsRows).toEqual([]); + }); + + it('emits cycle.stage.processed but skips analytics when the cycle has no reviews', async () => { + const processedEvents: any[] = []; + eventEmitter.on('cycle.stage.processed', (e) => + processedEvents.push(e), + ); + + await listener.handleCycleStageChanged( + new CycleStageChangedEvent( + cycleId, + CycleStage.ACTIVE, + CycleStage.FINISHED, + ), + ); + + expect(processedEvents).toHaveLength(1); + const analyticsRows = await analytics.getByCycleId(cycleId); + expect(analyticsRows).toEqual([]); + }); + + it('generates analytics when the cycle has reviews + cluster scores and emits processed', async () => { + const reviewId = await seedReview(); + await seedClusterScore(reviewId); + const processedEvents: any[] = []; + eventEmitter.on('cycle.stage.processed', (e) => + processedEvents.push(e), + ); + + await listener.handleCycleStageChanged( + new CycleStageChangedEvent( + cycleId, + CycleStage.ACTIVE, + CycleStage.FINISHED, + ), + ); + + expect(processedEvents).toHaveLength(1); + const analyticsRows = await analytics.getByCycleId(cycleId); + expect(analyticsRows).toHaveLength(1); + expect(analyticsRows[0].clusterId).toBe(clusterId); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/listeners/respondent-status.listener.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/listeners/respondent-status.listener.integration-spec.ts new file mode 100644 index 00000000..e1f4b87f --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/listeners/respondent-status.listener.integration-spec.ts @@ -0,0 +1,299 @@ +import '../../../setup-env'; + +import { + RespondentCategory, + ResponseStatus, + ReviewStage, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { RespondentStatusChangedEvent } from 'src/contexts/feedback360/application/events/respondent-status-changed.event'; +import { + RespondentStatusListener, + SelfAssessmentCompletedListener, +} from 'src/contexts/feedback360/application/listeners/respondent-status.listener'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { RespondentRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration)', () => { + let module: TestingModule; + let respondentListener: RespondentStatusListener; + let selfAssessmentListener: SelfAssessmentCompletedListener; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let respondentRepo: RespondentRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let hrId: number; + let rateeId: number; + let positionId: number; + let cycleId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + respondentListener = module.get(RespondentStatusListener); + selfAssessmentListener = module.get(SelfAssessmentCompletedListener); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + respondentRepo = module.get(RespondentRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + cycleId = cycle.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function createReview(stage: ReviewStage): Promise { + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId, + stage, + }), + ); + return review.id!; + } + + async function newUserId(seed: string): Promise { + const user = await identityUsers.create({ + firstName: seed, + lastName: 'P', + email: `${seed}.${Date.now()}.${Math.random()}@example.com`, + } as any); + return user.id!; + } + + async function addRespondent( + reviewId: number, + category: RespondentCategory, + status: ResponseStatus, + respondentId?: number, + ): Promise { + const id = respondentId ?? (await newUserId('R')); + const respondent = await respondentRepo.create( + RespondentDomain.create({ + reviewId, + respondentId: id, + fullName: 'R P', + category, + responseStatus: status, + positionId, + positionTitle: 'Engineer', + }), + ); + return respondent.id!; + } + + describe('RespondentStatusListener.handleRespondentStatusChanged', () => { + it('does nothing when status is not COMPLETED or CANCELED', async () => { + const reviewId = await createReview(ReviewStage.IN_PROGRESS); + const respondentRelationId = await addRespondent( + reviewId, + RespondentCategory.TEAM, + ResponseStatus.IN_PROGRESS, + ); + + await respondentListener.handleRespondentStatusChanged( + new RespondentStatusChangedEvent( + reviewId, + respondentRelationId, + ResponseStatus.PENDING, + ResponseStatus.IN_PROGRESS, + ), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.IN_PROGRESS); + }); + + it('completes the review when all respondents are COMPLETED', async () => { + const reviewId = await createReview(ReviewStage.IN_PROGRESS); + const respondentId = await addRespondent( + reviewId, + RespondentCategory.TEAM, + ResponseStatus.COMPLETED, + ); + + await respondentListener.handleRespondentStatusChanged( + new RespondentStatusChangedEvent( + reviewId, + respondentId, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.FINISHED); + }); + + it('does not complete the review when some respondents are still PENDING', async () => { + const reviewId = await createReview(ReviewStage.IN_PROGRESS); + const userA = await newUserId('A'); + const userB = await newUserId('B'); + const relA = await addRespondent( + reviewId, + RespondentCategory.TEAM, + ResponseStatus.COMPLETED, + userA, + ); + await addRespondent( + reviewId, + RespondentCategory.TEAM, + ResponseStatus.PENDING, + userB, + ); + + await respondentListener.handleRespondentStatusChanged( + new RespondentStatusChangedEvent( + reviewId, + relA, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.IN_PROGRESS); + }); + }); + + describe('SelfAssessmentCompletedListener.handleRespondentCompleted', () => { + it('advances the review SELF_ASSESSMENT → IN_PROGRESS when the ratee completes self-assessment', async () => { + const reviewId = await createReview(ReviewStage.SELF_ASSESSMENT); + const respondentId = await addRespondent( + reviewId, + RespondentCategory.SELF_ASSESSMENT, + ResponseStatus.COMPLETED, + rateeId, + ); + + await selfAssessmentListener.handleRespondentCompleted( + new RespondentStatusChangedEvent( + reviewId, + respondentId, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.IN_PROGRESS); + }); + + it('skips the transition when the completed respondent is not a SELF_ASSESSMENT', async () => { + const reviewId = await createReview(ReviewStage.SELF_ASSESSMENT); + const respondentId = await addRespondent( + reviewId, + RespondentCategory.TEAM, + ResponseStatus.COMPLETED, + ); + + await selfAssessmentListener.handleRespondentCompleted( + new RespondentStatusChangedEvent( + reviewId, + respondentId, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.SELF_ASSESSMENT); + }); + + it('skips the transition when the review is not in SELF_ASSESSMENT stage', async () => { + const reviewId = await createReview(ReviewStage.IN_PROGRESS); + const respondentId = await addRespondent( + reviewId, + RespondentCategory.SELF_ASSESSMENT, + ResponseStatus.COMPLETED, + rateeId, + ); + + await selfAssessmentListener.handleRespondentCompleted( + new RespondentStatusChangedEvent( + reviewId, + respondentId, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.IN_PROGRESS); + }); + + it('does nothing when toStatus is not COMPLETED', async () => { + const reviewId = await createReview(ReviewStage.SELF_ASSESSMENT); + const respondentId = await addRespondent( + reviewId, + RespondentCategory.SELF_ASSESSMENT, + ResponseStatus.IN_PROGRESS, + rateeId, + ); + + await selfAssessmentListener.handleRespondentCompleted( + new RespondentStatusChangedEvent( + reviewId, + respondentId, + ResponseStatus.PENDING, + ResponseStatus.IN_PROGRESS, + ), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.SELF_ASSESSMENT); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/listeners/review-stage.listener.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/listeners/review-stage.listener.integration-spec.ts new file mode 100644 index 00000000..7fdce28b --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/listeners/review-stage.listener.integration-spec.ts @@ -0,0 +1,186 @@ +import '../../../setup-env'; + +import { CycleStage, ReviewStage } from '@intra/shared-kernel'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TestingModule } from '@nestjs/testing'; +import { ReviewStageChangedEvent } from 'src/contexts/feedback360/application/events/review-stage-changed.event'; +import { ReviewStageListener } from 'src/contexts/feedback360/application/listeners/review-stage.listener'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ReviewStageListener (feedback360, integration)', () => { + let module: TestingModule; + let listener: ReviewStageListener; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let eventEmitter: EventEmitter2; + let prisma: PrismaService; + + let hrId: number; + let rateeId: number; + let positionId: number; + let cycleId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + listener = module.get(ReviewStageListener); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + eventEmitter = module.get(EventEmitter2); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + // Manually persist the cycle in ACTIVE stage so completeCycle() + // is allowed to transition NEW → ... well ACTIVE → FINISHED. + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + stage: CycleStage.ACTIVE, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + cycleId = cycle.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function createReview(stage: ReviewStage): Promise { + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId, + stage, + }), + ); + return review.id!; + } + + describe('handleReviewStageChanged', () => { + it('does nothing when toStage is not FINISHED and emits review.stage.processed', async () => { + const reviewId = await createReview(ReviewStage.NEW); + const processedEvents: any[] = []; + eventEmitter.on('review.stage.processed', (e) => + processedEvents.push(e), + ); + + await listener.handleReviewStageChanged( + new ReviewStageChangedEvent( + reviewId, + ReviewStage.NEW, + ReviewStage.SELF_ASSESSMENT, + ), + ); + + expect(processedEvents).toHaveLength(1); + expect(processedEvents[0].currentStage).toBe( + ReviewStage.SELF_ASSESSMENT, + ); + + const cycleAfter = await cycleRepo.findById(cycleId); + expect(cycleAfter!.stage).toBe(CycleStage.ACTIVE); + }); + + it('completes the cycle when this is the last active review in it', async () => { + const reviewId = await createReview(ReviewStage.FINISHED); + + await listener.handleReviewStageChanged( + new ReviewStageChangedEvent( + reviewId, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ), + ); + + const cycleAfter = await cycleRepo.findById(cycleId); + expect(cycleAfter!.stage).toBe(CycleStage.FINISHED); + }); + + it('does not complete the cycle when other reviews are still in progress', async () => { + const finishedReviewId = await createReview(ReviewStage.FINISHED); + await createReview(ReviewStage.IN_PROGRESS); + + await listener.handleReviewStageChanged( + new ReviewStageChangedEvent( + finishedReviewId, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ), + ); + + const cycleAfter = await cycleRepo.findById(cycleId); + expect(cycleAfter!.stage).toBe(CycleStage.ACTIVE); + }); + + it('still emits review.stage.processed when the review has no cycle attached', async () => { + // Review with cycleId=null + const orphanReview = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId: null, + stage: ReviewStage.FINISHED, + }), + ); + const processedEvents: any[] = []; + eventEmitter.on('review.stage.processed', (e) => + processedEvents.push(e), + ); + + await listener.handleReviewStageChanged( + new ReviewStageChangedEvent( + orphanReview.id!, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ), + ); + + expect(processedEvents).toHaveLength(1); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/answer.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/answer.repository.integration-spec.ts new file mode 100644 index 00000000..4fd4395d --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/answer.repository.integration-spec.ts @@ -0,0 +1,278 @@ +import '../../../setup-env'; + +import { + AnswerSortField, + AnswerType, + RespondentCategory, + ReviewStage, + SortDirection, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { AnswerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('AnswerRepository (integration)', () => { + let module: TestingModule; + let repo: AnswerRepository; + let reviews: ReviewRepository; + let questions: QuestionRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let reviewId: number; + let questionId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(AnswerRepository); + reviews = module.get(ReviewRepository); + questions = module.get(QuestionRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.IN_PROGRESS, + }), + ); + const question = await questions.create( + QuestionDomain.create({ + cycleId: cycle.id!, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + }), + ); + + reviewId = review.id!; + questionId = question.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create / findById / list', () => { + it('persists a numerical answer', async () => { + const created = await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }), + ); + + expect(created.id).toBeDefined(); + expect(created.numericalValue).toBe(4); + expect(created.textValue).toBeNull(); + }); + + it('persists a text answer', async () => { + const created = await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.OTHER, + answerType: AnswerType.TEXT_FIELD, + textValue: 'Great teamwork', + }), + ); + + expect(created.textValue).toBe('Great teamwork'); + expect(created.numericalValue).toBeNull(); + }); + + it('findById returns the answer when found', async () => { + const created = await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 5, + }), + ); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + AnswerDomain, + ); + }); + + it('findById returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + + it('list filters by reviewId', async () => { + await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 3, + }), + ); + + const result = await repo.list({ reviewId } as any); + + expect(result).toHaveLength(1); + }); + + it('list filters by respondentCategory', async () => { + await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 3, + }), + ); + await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.SELF_ASSESSMENT, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }), + ); + + const result = await repo.list({ + respondentCategory: RespondentCategory.SELF_ASSESSMENT, + } as any); + + expect(result).toHaveLength(1); + expect(result[0].numericalValue).toBe(4); + }); + + it('list honours sort direction', async () => { + await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 1, + }), + ); + await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 5, + }), + ); + + const result = await repo.list({ + sortBy: AnswerSortField.NUMERICAL_VALUE, + sortDirection: SortDirection.DESC, + } as any); + + expect(result[0].numericalValue).toBe(5); + }); + }); + + describe('getAnswersCountByRespondentCategories', () => { + it('returns an empty list when there are no answers', async () => { + const result = + await repo.getAnswersCountByRespondentCategories(reviewId); + + expect(result).toEqual([]); + }); + + it('counts answers per respondent and aggregates by category', async () => { + // 2 team respondents, both answered the single question + for (let i = 0; i < 2; i++) { + await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }), + ); + } + + const result = + await repo.getAnswersCountByRespondentCategories(reviewId); + + expect(result).toEqual([ + { respondentCategory: RespondentCategory.TEAM, answers: 2 }, + ]); + }); + }); + + describe('deleteById', () => { + it('removes the answer row', async () => { + const created = await repo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.answer.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/cluster-score-analytics.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/cluster-score-analytics.repository.integration-spec.ts new file mode 100644 index 00000000..f9e39625 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/cluster-score-analytics.repository.integration-spec.ts @@ -0,0 +1,186 @@ +import '../../../setup-env'; + +import { TestingModule } from '@nestjs/testing'; +import { ClusterScoreAnalyticsDomain } from 'src/contexts/feedback360/domain/cluster-score-analytics.domain'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ClusterScoreAnalyticsRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { ClusterRepository } from 'src/contexts/library/infrastructure/prisma-repositories/cluster.repository'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ClusterScoreAnalyticsRepository (integration)', () => { + let module: TestingModule; + let repo: ClusterScoreAnalyticsRepository; + let cycles: CycleRepository; + let competences: CompetenceRepository; + let clusters: ClusterRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let cycleId: number; + let clusterId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(ClusterScoreAnalyticsRepository); + cycles = module.get(CycleRepository); + competences = module.get(CompetenceRepository); + clusters = module.get(ClusterRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const cluster = await clusters.create( + ClusterDomain.create({ + competenceId: competence.id!, + lowerBound: 0, + upperBound: 3, + title: 'Beginner', + description: '', + }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + + cycleId = cycle.id!; + clusterId = cluster.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildAnalytics( + overrides: Partial<{ + averageScore: number; + employeesCount: number; + }> = {}, + ): ClusterScoreAnalyticsDomain { + return ClusterScoreAnalyticsDomain.create({ + cycleId, + clusterId, + lowerBound: 0, + upperBound: 3, + employeesCount: overrides.employeesCount ?? 5, + employeeDensity: 0.5, + minScore: 1, + maxScore: 4, + averageScore: overrides.averageScore ?? 2.5, + }); + } + + describe('upsert', () => { + it('creates a new analytics row', async () => { + const saved = await repo.upsert(buildAnalytics()); + + expect(saved.id).toBeDefined(); + expect(saved.averageScore.toNumber()).toBe(2.5); + }); + + it('updates the existing row when (cycleId, clusterId) match', async () => { + const first = await repo.upsert(buildAnalytics()); + const second = await repo.upsert( + buildAnalytics({ averageScore: 3.7, employeesCount: 7 }), + ); + + expect(second.id).toBe(first.id); + expect(second.averageScore.toNumber()).toBe(3.7); + + const rows = await prisma.clusterScoreAnalytics.findMany({ + where: { cycleId, clusterId }, + }); + expect(rows).toHaveLength(1); + }); + }); + + describe('findById', () => { + it('returns the row when found', async () => { + const saved = await repo.upsert(buildAnalytics()); + + await expect(repo.findById(saved.id!)).resolves.toBeInstanceOf( + ClusterScoreAnalyticsDomain, + ); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + it('filters by cycleId', async () => { + await repo.upsert(buildAnalytics()); + + const result = await repo.search({ cycleId } as any); + + expect(result).toHaveLength(1); + }); + + it('filters by clusterId', async () => { + await repo.upsert(buildAnalytics()); + + const result = await repo.search({ clusterId } as any); + + expect(result).toHaveLength(1); + }); + }); + + describe('updateById', () => { + it('persists patched fields', async () => { + const saved = await repo.upsert(buildAnalytics()); + + const updated = await repo.updateById(saved.id!, { + employeesCount: 10, + } as any); + + expect(updated.employeesCount.toNumber()).toBe(10); + }); + }); + + describe('getByCycleId', () => { + it('returns analytics tied to a cycle', async () => { + await repo.upsert(buildAnalytics()); + + const result = await repo.getByCycleId(cycleId); + + expect(result).toHaveLength(1); + }); + }); + + describe('deleteById', () => { + it('removes the row', async () => { + const saved = await repo.upsert(buildAnalytics()); + + await repo.deleteById(saved.id!); + + const fromDb = await prisma.clusterScoreAnalytics.findUnique({ + where: { id: saved.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/cluster-score.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/cluster-score.repository.integration-spec.ts new file mode 100644 index 00000000..fd8a6d88 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/cluster-score.repository.integration-spec.ts @@ -0,0 +1,203 @@ +import '../../../setup-env'; + +import { ReviewStage } from '@intra/shared-kernel'; +import { NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { ClusterScoreWithRelationsDomain } from 'src/contexts/feedback360/domain/cluster-score-with-relations.domain'; +import { ClusterScoreDomain } from 'src/contexts/feedback360/domain/cluster-score.domain'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ClusterScoreRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { ClusterRepository } from 'src/contexts/library/infrastructure/prisma-repositories/cluster.repository'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ClusterScoreRepository (integration)', () => { + let module: TestingModule; + let repo: ClusterScoreRepository; + let reviews: ReviewRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let competences: CompetenceRepository; + let clusters: ClusterRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let cycleId: number; + let reviewId: number; + let rateeId: number; + let clusterId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(ClusterScoreRepository); + reviews = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + competences = module.get(CompetenceRepository); + clusters = module.get(ClusterRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const cluster = await clusters.create( + ClusterDomain.create({ + competenceId: competence.id!, + lowerBound: 0, + upperBound: 3, + title: 'Beginner', + description: '', + }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + + cycleId = cycle.id!; + reviewId = review.id!; + rateeId = ratee.id!; + clusterId = cluster.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildScore(score: number = 3.5): ClusterScoreDomain { + return ClusterScoreDomain.create({ + cycleId, + clusterId, + rateeId, + reviewId, + score, + answersCount: 5, + }); + } + + describe('upsert', () => { + it('creates a new score row', async () => { + const saved = await repo.upsert(buildScore(3.5)); + + expect(saved.id).toBeDefined(); + expect(saved.score.toNumber()).toBe(3.5); + expect(saved.answersCount).toBe(5); + }); + + it('updates the existing row when (clusterId, rateeId) match', async () => { + const first = await repo.upsert(buildScore(3.5)); + const second = await repo.upsert(buildScore(4.2)); + + expect(second.id).toBe(first.id); + expect(second.score.toNumber()).toBe(4.2); + + const rows = await prisma.clusterScore.findMany({ + where: { clusterId, rateeId }, + }); + expect(rows).toHaveLength(1); + }); + }); + + describe('list', () => { + it('filters by cycleId', async () => { + await repo.upsert(buildScore(3.5)); + + const result = await repo.list({ cycleId } as any); + expect(result).toHaveLength(1); + }); + + it('filters by clusterId', async () => { + await repo.upsert(buildScore(3.5)); + + const result = await repo.list({ clusterId } as any); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('returns the score with cluster and ratee relations', async () => { + const saved = await repo.upsert(buildScore(3.5)); + + const fetched = await repo.getById(saved.id!); + + expect(fetched).toBeInstanceOf(ClusterScoreWithRelationsDomain); + expect(fetched.cluster.id).toBe(clusterId); + expect(fetched.ratee.id).toBe(rateeId); + }); + + it('throws NotFoundException for a missing id', async () => { + await expect(repo.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('getByCycleId', () => { + it('returns scores tied to the cycle with relations', async () => { + await repo.upsert(buildScore(3.5)); + + const result = await repo.getByCycleId(cycleId); + + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(ClusterScoreWithRelationsDomain); + }); + }); + + describe('deleteById', () => { + it('removes the row', async () => { + const saved = await repo.upsert(buildScore(3.5)); + + await repo.deleteById(saved.id!); + + const fromDb = await prisma.clusterScore.findUnique({ + where: { id: saved.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/cycle-stage-history.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/cycle-stage-history.repository.integration-spec.ts new file mode 100644 index 00000000..46402364 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/cycle-stage-history.repository.integration-spec.ts @@ -0,0 +1,122 @@ +import '../../../setup-env'; + +import { CycleStage } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleStageHistoryDomain } from 'src/contexts/feedback360/domain/cycle-stage-history.domain'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { CycleStageHistoryRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('CycleStageHistoryRepository (integration)', () => { + let module: TestingModule; + let repo: CycleStageHistoryRepository; + let cycles: CycleRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let cycleId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(CycleStageHistoryRepository); + cycles = module.get(CycleRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + cycleId = cycle.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a new history row', async () => { + const created = await repo.create( + CycleStageHistoryDomain.create({ + cycleId, + fromStage: CycleStage.NEW, + toStage: CycleStage.ACTIVE, + changedById: null, + changedByName: 'system', + reason: 'manual trigger', + }), + ); + + expect(created.id).toBeDefined(); + expect(created.fromStage).toBe(CycleStage.NEW); + expect(created.toStage).toBe(CycleStage.ACTIVE); + }); + }); + + describe('findByCycleId', () => { + it('returns rows in ascending createdAt order', async () => { + await repo.create( + CycleStageHistoryDomain.create({ + cycleId, + fromStage: CycleStage.NEW, + toStage: CycleStage.ACTIVE, + }), + ); + await repo.create( + CycleStageHistoryDomain.create({ + cycleId, + fromStage: CycleStage.ACTIVE, + toStage: CycleStage.FINISHED, + }), + ); + + const rows = await repo.findByCycleId(cycleId); + + expect(rows.map((r) => r.toStage)).toEqual([ + CycleStage.ACTIVE, + CycleStage.FINISHED, + ]); + }); + + it('returns an empty array when no history exists', async () => { + await expect(repo.findByCycleId(cycleId)).resolves.toEqual([]); + }); + }); + + describe('findById', () => { + it('returns the row when found', async () => { + const created = await repo.create( + CycleStageHistoryDomain.create({ + cycleId, + fromStage: CycleStage.NEW, + toStage: CycleStage.ACTIVE, + }), + ); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + CycleStageHistoryDomain, + ); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/cycle.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/cycle.repository.integration-spec.ts new file mode 100644 index 00000000..7dd938c1 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/cycle.repository.integration-spec.ts @@ -0,0 +1,173 @@ +import '../../../setup-env'; + +import { + CycleSortField, + CycleStage, + SortDirection, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('CycleRepository (integration)', () => { + let module: TestingModule; + let repo: CycleRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let hrId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(CycleRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + hrId = hr.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildCycle( + overrides: Partial<{ + title: string; + stage: CycleStage; + isActive: boolean; + startDate: Date; + endDate: Date; + }> = {}, + ): CycleDomain { + return CycleDomain.create({ + title: overrides.title ?? 'Q1 2026', + description: 'A quarterly cycle', + hrId, + stage: overrides.stage ?? CycleStage.NEW, + isActive: overrides.isActive ?? true, + startDate: overrides.startDate ?? new Date('2026-01-01'), + endDate: overrides.endDate ?? new Date('2026-03-31'), + }); + } + + describe('create / findById', () => { + it('persists a new cycle with defaults applied', async () => { + const created = await repo.create(buildCycle()); + + expect(created.id).toBeDefined(); + expect(created.title).toBe('Q1 2026'); + expect(created.stage).toBe(CycleStage.NEW); + + const fromDb = await prisma.cycle.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Q1 2026'); + expect(fromDb!.hrId).toBe(hrId); + }); + + it('findById returns the cycle when found', async () => { + const created = await repo.create(buildCycle()); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + CycleDomain, + ); + }); + + it('findById returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await repo.create(buildCycle({ title: 'Alpha cycle' })); + await repo.create( + buildCycle({ title: 'Beta cycle', stage: CycleStage.ACTIVE }), + ); + await repo.create( + buildCycle({ title: 'Gamma cycle', isActive: false }), + ); + }); + + it('returns all cycles when no filter is supplied', async () => { + const all = await repo.search({} as any); + expect(all).toHaveLength(3); + }); + + it('filters by title (case insensitive substring)', async () => { + const result = await repo.search({ title: 'beta' } as any); + expect(result.map((c) => c.title)).toEqual(['Beta cycle']); + }); + + it('filters by stage', async () => { + const result = await repo.search({ + stage: CycleStage.ACTIVE, + } as any); + expect(result.map((c) => c.title)).toEqual(['Beta cycle']); + }); + + it('filters by isActive', async () => { + const result = await repo.search({ isActive: false } as any); + expect(result.map((c) => c.title)).toEqual(['Gamma cycle']); + }); + + it('honours descending sort on title', async () => { + const result = await repo.search({ + sortBy: CycleSortField.TITLE, + sortDirection: SortDirection.DESC, + } as any); + const titles = result.map((c) => c.title); + expect(titles).toEqual([...titles].sort().reverse()); + }); + }); + + describe('updateById', () => { + it('persists the patched scalar fields', async () => { + const created = await repo.create(buildCycle()); + + const updated = await repo.updateById(created.id!, { + title: 'Renamed', + description: 'updated', + } as any); + + expect(updated.title).toBe('Renamed'); + expect(updated.description).toBe('updated'); + }); + + it('updates the cycle stage when patched', async () => { + const created = await repo.create(buildCycle()); + + const updated = await repo.updateById(created.id!, { + stage: CycleStage.ACTIVE, + } as any); + + expect(updated.stage).toBe(CycleStage.ACTIVE); + }); + }); + + describe('deleteById', () => { + it('removes the cycle row', async () => { + const created = await repo.create(buildCycle()); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.cycle.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/question.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/question.repository.integration-spec.ts new file mode 100644 index 00000000..cc56498d --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/question.repository.integration-spec.ts @@ -0,0 +1,168 @@ +import '../../../setup-env'; + +import { + AnswerType, + QuestionSortField, + SortDirection, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('QuestionRepository (integration)', () => { + let module: TestingModule; + let repo: QuestionRepository; + let cycles: CycleRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let cycleId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(QuestionRepository); + cycles = module.get(CycleRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + cycleId = cycle.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create / findById', () => { + it('persists a new question with defaults applied', async () => { + const created = await repo.create( + QuestionDomain.create({ + cycleId, + title: 'Rate teamwork', + answerType: AnswerType.NUMERICAL_SCALE, + }), + ); + + expect(created.id).toBeDefined(); + expect(created.isForSelfassessment).toBe(false); + expect(created.title).toBe('Rate teamwork'); + }); + + it('findById returns the row when found', async () => { + const created = await repo.create( + QuestionDomain.create({ + cycleId, + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + }), + ); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + QuestionDomain, + ); + }); + + it('findById returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await repo.create( + QuestionDomain.create({ + cycleId, + title: 'Numeric Q', + answerType: AnswerType.NUMERICAL_SCALE, + isForSelfassessment: true, + }), + ); + await repo.create( + QuestionDomain.create({ + cycleId, + title: 'Text Q', + answerType: AnswerType.TEXT_FIELD, + }), + ); + }); + + it('returns all questions when no filter is supplied', async () => { + const all = await repo.search({} as any); + expect(all).toHaveLength(2); + }); + + it('filters by cycleId', async () => { + const result = await repo.search({ cycleId } as any); + expect(result).toHaveLength(2); + }); + + it('filters by answerType', async () => { + const result = await repo.search({ + answerType: AnswerType.TEXT_FIELD, + } as any); + expect(result.map((q) => q.title)).toEqual(['Text Q']); + }); + + it('filters by isForSelfassessment = true', async () => { + const result = await repo.search({ + isForSelfassessment: true, + } as any); + expect(result.map((q) => q.title)).toEqual(['Numeric Q']); + }); + + it('filters by title (case insensitive substring)', async () => { + const result = await repo.search({ title: 'NUMERIC' } as any); + expect(result.map((q) => q.title)).toEqual(['Numeric Q']); + }); + + it('honours descending sort on title', async () => { + const result = await repo.search({ + sortBy: QuestionSortField.TITLE, + sortDirection: SortDirection.DESC, + } as any); + const titles = result.map((q) => q.title); + expect(titles).toEqual([...titles].sort().reverse()); + }); + }); + + describe('deleteById', () => { + it('removes the question row', async () => { + const created = await repo.create( + QuestionDomain.create({ + cycleId, + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.question.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/respondent.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/respondent.repository.integration-spec.ts new file mode 100644 index 00000000..fe2a3254 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/respondent.repository.integration-spec.ts @@ -0,0 +1,244 @@ +import '../../../setup-env'; + +import { + RespondentCategory, + RespondentSortField, + ResponseStatus, + ReviewStage, + SortDirection, +} from '@intra/shared-kernel'; +import { NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { RespondentRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('RespondentRepository (integration)', () => { + let module: TestingModule; + let repo: RespondentRepository; + let reviews: ReviewRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let reviewId: number; + let respondentUserId: number; + let positionId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(RespondentRepository); + reviews = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const respondentUser = await identityUsers.create({ + firstName: 'Sara', + lastName: 'Lopez', + email: `respondent.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.NEW, + }), + ); + + reviewId = review.id!; + respondentUserId = respondentUser.id!; + positionId = position.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildRespondent( + overrides: Partial<{ + category: RespondentCategory; + responseStatus: ResponseStatus; + fullName: string; + respondentId: number; + }> = {}, + ): RespondentDomain { + return RespondentDomain.create({ + reviewId, + respondentId: overrides.respondentId ?? respondentUserId, + fullName: overrides.fullName ?? 'Sara Lopez', + category: overrides.category ?? RespondentCategory.TEAM, + responseStatus: overrides.responseStatus ?? ResponseStatus.PENDING, + positionId, + positionTitle: 'Engineer', + }); + } + + async function newUserId(seed: string): Promise { + const user = await identityUsers.create({ + firstName: seed, + lastName: 'Person', + email: `${seed}.${Date.now()}.${Math.random()}@example.com`, + } as any); + return user.id!; + } + + describe('create / getById', () => { + it('persists a respondent with defaults applied', async () => { + const created = await repo.create(buildRespondent()); + + expect(created.id).toBeDefined(); + expect(created.category).toBe(RespondentCategory.TEAM); + expect(created.responseStatus).toBe(ResponseStatus.PENDING); + }); + + it('getById returns the respondent when found', async () => { + const created = await repo.create(buildRespondent()); + + await expect(repo.getById(created.id!)).resolves.toBeInstanceOf( + RespondentDomain, + ); + }); + + it('getById throws NotFoundException when missing', async () => { + await expect(repo.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('listByReview', () => { + it('returns respondents filtered by category', async () => { + const otherId = await newUserId('Robert'); + await repo.create(buildRespondent()); + await repo.create( + buildRespondent({ + category: RespondentCategory.SELF_ASSESSMENT, + fullName: 'Robert Smith', + respondentId: otherId, + }), + ); + + const result = await repo.listByReview(reviewId, { + category: RespondentCategory.SELF_ASSESSMENT, + } as any); + + expect(result).toHaveLength(1); + expect(result[0].fullName).toBe('Robert Smith'); + }); + + it('returns respondents filtered by responseStatus', async () => { + const otherId = await newUserId('Other'); + await repo.create(buildRespondent()); + await repo.create( + buildRespondent({ + responseStatus: ResponseStatus.COMPLETED, + fullName: 'Other Name', + respondentId: otherId, + }), + ); + + const result = await repo.listByReview(reviewId, { + responseStatus: ResponseStatus.COMPLETED, + } as any); + + expect(result).toHaveLength(1); + expect(result[0].fullName).toBe('Other Name'); + }); + + it('honours sort direction', async () => { + const aaId = await newUserId('Aa'); + const zzId = await newUserId('Zz'); + await repo.create( + buildRespondent({ + fullName: 'Aa Person', + respondentId: aaId, + }), + ); + await repo.create( + buildRespondent({ + fullName: 'Zz Person', + respondentId: zzId, + }), + ); + + const result = await repo.listByReview(reviewId, { + sortBy: RespondentSortField.FULL_NAME, + sortDirection: SortDirection.DESC, + } as any); + + expect(result.map((r) => r.fullName)).toEqual([ + 'Zz Person', + 'Aa Person', + ]); + }); + }); + + describe('updateById', () => { + it('persists patched fields with enum conversion', async () => { + const created = await repo.create(buildRespondent()); + + const updated = await repo.updateById(created.id!, { + responseStatus: ResponseStatus.COMPLETED, + hrNote: 'completed on time', + } as any); + + expect(updated.responseStatus).toBe(ResponseStatus.COMPLETED); + expect(updated.hrNote).toBe('completed on time'); + }); + }); + + describe('deleteById', () => { + it('removes the respondent row', async () => { + const created = await repo.create(buildRespondent()); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.respondent.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/review-question-relation.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/review-question-relation.repository.integration-spec.ts new file mode 100644 index 00000000..11f62516 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/review-question-relation.repository.integration-spec.ts @@ -0,0 +1,177 @@ +import '../../../setup-env'; + +import { AnswerType, ReviewStage } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { ReviewQuestionRelationRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review-question-relation.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ReviewQuestionRelationRepository (integration)', () => { + let module: TestingModule; + let repo: ReviewQuestionRelationRepository; + let reviews: ReviewRepository; + let questions: QuestionRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let competences: CompetenceRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let reviewId: number; + let questionId: number; + let competenceId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(ReviewQuestionRelationRepository); + reviews = module.get(ReviewRepository); + questions = module.get(QuestionRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + competences = module.get(CompetenceRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.NEW, + }), + ); + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const question = await questions.create( + QuestionDomain.create({ + cycleId: cycle.id!, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: competence.id!, + }), + ); + + reviewId = review.id!; + questionId = question.id!; + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildRelation( + overrides: Partial<{ questionTitle: string }> = {}, + ): ReviewQuestionRelationDomain { + return ReviewQuestionRelationDomain.create({ + reviewId, + questionId, + questionTitle: overrides.questionTitle ?? 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId, + competenceTitle: 'Teamwork', + }); + } + + describe('link', () => { + it('inserts a new relation', async () => { + const created = await repo.link(buildRelation()); + + expect(created.id).toBeDefined(); + expect(created.reviewId).toBe(reviewId); + expect(created.questionId).toBe(questionId); + }); + + it('updates an existing relation on conflict (upsert)', async () => { + const first = await repo.link(buildRelation()); + const second = await repo.link( + buildRelation({ questionTitle: 'Updated title' }), + ); + + expect(second.id).toBe(first.id); + expect(second.questionTitle).toBe('Updated title'); + + const rows = await prisma.reviewQuestionRelation.findMany({ + where: { reviewId, questionId }, + }); + expect(rows).toHaveLength(1); + }); + }); + + describe('listByReview', () => { + it('returns the relations attached to a review', async () => { + await repo.link(buildRelation()); + + const result = await repo.listByReview(reviewId, {} as any); + + expect(result).toHaveLength(1); + expect(result[0].competenceTitle).toBe('Teamwork'); + }); + + it('filters by questionTitle (case insensitive)', async () => { + await repo.link(buildRelation({ questionTitle: 'Alpha?' })); + + const result = await repo.listByReview(reviewId, { + questionTitle: 'alpha', + } as any); + + expect(result).toHaveLength(1); + }); + }); + + describe('unlink', () => { + it('removes the relation', async () => { + await repo.link(buildRelation()); + + await repo.unlink(reviewId, questionId); + + const rows = await prisma.reviewQuestionRelation.findMany({ + where: { reviewId, questionId }, + }); + expect(rows).toEqual([]); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/review-stage-history.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/review-stage-history.repository.integration-spec.ts new file mode 100644 index 00000000..33de1ef1 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/review-stage-history.repository.integration-spec.ts @@ -0,0 +1,149 @@ +import '../../../setup-env'; + +import { ReviewStage } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewStageHistoryDomain } from 'src/contexts/feedback360/domain/review-stage-history.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewStageHistoryRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review-stage-history.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ReviewStageHistoryRepository (integration)', () => { + let module: TestingModule; + let repo: ReviewStageHistoryRepository; + let reviews: ReviewRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let reviewId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(ReviewStageHistoryRepository); + reviews = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.NEW, + }), + ); + reviewId = review.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a history row', async () => { + const created = await repo.create( + ReviewStageHistoryDomain.create({ + reviewId, + fromStage: ReviewStage.NEW, + toStage: ReviewStage.IN_PROGRESS, + changedByName: 'system', + reason: 'manual trigger', + }), + ); + + expect(created.id).toBeDefined(); + expect(created.fromStage).toBe(ReviewStage.NEW); + expect(created.toStage).toBe(ReviewStage.IN_PROGRESS); + }); + }); + + describe('findByReviewId', () => { + it('returns rows in ascending createdAt order', async () => { + await repo.create( + ReviewStageHistoryDomain.create({ + reviewId, + fromStage: ReviewStage.NEW, + toStage: ReviewStage.IN_PROGRESS, + }), + ); + await repo.create( + ReviewStageHistoryDomain.create({ + reviewId, + fromStage: ReviewStage.IN_PROGRESS, + toStage: ReviewStage.FINISHED, + }), + ); + + const rows = await repo.findByReviewId(reviewId); + + expect(rows.map((r) => r.toStage)).toEqual([ + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ]); + }); + + it('returns an empty array when no history exists', async () => { + await expect(repo.findByReviewId(reviewId)).resolves.toEqual([]); + }); + }); + + describe('findById', () => { + it('returns the row when found', async () => { + const created = await repo.create( + ReviewStageHistoryDomain.create({ + reviewId, + fromStage: ReviewStage.NEW, + toStage: ReviewStage.IN_PROGRESS, + }), + ); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + ReviewStageHistoryDomain, + ); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/review.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/review.repository.integration-spec.ts new file mode 100644 index 00000000..25bc9ca4 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/review.repository.integration-spec.ts @@ -0,0 +1,190 @@ +import '../../../setup-env'; + +import { + ReviewSortField, + ReviewStage, + SortDirection, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ReviewRepository (integration)', () => { + let module: TestingModule; + let repo: ReviewRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let hrId: number; + let rateeId: number; + let rateePositionId: number; + let cycleId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + + hrId = hr.id!; + rateeId = ratee.id!; + rateePositionId = position.id!; + cycleId = cycle.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildReview( + overrides: Partial<{ + stage: ReviewStage; + hrNote: string; + }> = {}, + ): ReviewDomain { + return ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + hrNote: overrides.hrNote ?? null, + cycleId, + stage: overrides.stage ?? ReviewStage.NEW, + }); + } + + describe('create / findById', () => { + it('persists a review with defaults applied', async () => { + const created = await repo.create(buildReview()); + + expect(created.id).toBeDefined(); + expect(created.rateeId).toBe(rateeId); + expect(created.stage).toBe(ReviewStage.NEW); + }); + + it('findById returns the review when found', async () => { + const created = await repo.create(buildReview()); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + ReviewDomain, + ); + }); + + it('findById returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('search / listByCycleId', () => { + beforeEach(async () => { + await repo.create(buildReview()); + await repo.create(buildReview({ stage: ReviewStage.IN_PROGRESS })); + await repo.create(buildReview({ hrNote: 'second cycle' })); + }); + + it('returns all reviews when no filter is supplied', async () => { + const all = await repo.search({} as any); + expect(all).toHaveLength(3); + }); + + it('filters by stage', async () => { + const result = await repo.search({ + stage: ReviewStage.IN_PROGRESS, + } as any); + expect(result).toHaveLength(1); + }); + + it('filters by hrId', async () => { + const result = await repo.search({ hrId } as any); + expect(result).toHaveLength(3); + }); + + it('listByCycleId returns reviews tied to a cycle', async () => { + const result = await repo.listByCycleId(cycleId); + expect(result).toHaveLength(3); + }); + + it('honours descending sort on rateeFullName', async () => { + const result = await repo.search({ + sortBy: ReviewSortField.RATEE_FULL_NAME, + sortDirection: SortDirection.DESC, + } as any); + expect(result).toHaveLength(3); + }); + }); + + describe('updateById', () => { + it('persists patched scalar fields', async () => { + const created = await repo.create(buildReview()); + + const updated = await repo.updateById(created.id!, { + hrNote: 'updated', + } as any); + + expect(updated.hrNote).toBe('updated'); + }); + + it('updates the stage when patched', async () => { + const created = await repo.create(buildReview()); + + const updated = await repo.updateById(created.id!, { + stage: ReviewStage.IN_PROGRESS, + } as any); + + expect(updated.stage).toBe(ReviewStage.IN_PROGRESS); + }); + }); + + describe('deleteById', () => { + it('cascades cleanup of dependent rows in a single transaction', async () => { + const created = await repo.create(buildReview()); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.review.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/repository/reviewer.repository.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/repository/reviewer.repository.integration-spec.ts new file mode 100644 index 00000000..9aba0db8 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/repository/reviewer.repository.integration-spec.ts @@ -0,0 +1,207 @@ +import '../../../setup-env'; + +import { + ReviewerSortField, + ReviewStage, + SortDirection, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { ReviewerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/reviewer.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ReviewerRepository (integration)', () => { + let module: TestingModule; + let repo: ReviewerRepository; + let reviews: ReviewRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let reviewId: number; + let reviewerUserId: number; + let positionId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + repo = module.get(ReviewerRepository); + reviews = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const reviewer = await identityUsers.create({ + firstName: 'Sara', + lastName: 'Lopez', + email: `reviewer.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.NEW, + }), + ); + + reviewId = review.id!; + reviewerUserId = reviewer.id!; + positionId = position.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a reviewer row', async () => { + const created = await repo.create( + ReviewerDomain.create({ + reviewId, + reviewerId: reviewerUserId, + fullName: 'Sara Lopez', + positionId, + positionTitle: 'Engineer', + }), + ); + + expect(created.id).toBeDefined(); + expect(created.reviewerId).toBe(reviewerUserId); + }); + }); + + describe('listByReview', () => { + it('returns reviewers for a given review', async () => { + await repo.create( + ReviewerDomain.create({ + reviewId, + reviewerId: reviewerUserId, + fullName: 'Sara Lopez', + positionId, + positionTitle: 'Engineer', + }), + ); + + const result = await repo.listByReview(reviewId, {} as any); + + expect(result).toHaveLength(1); + expect(result[0].fullName).toBe('Sara Lopez'); + }); + + it('filters by fullName (case insensitive)', async () => { + await repo.create( + ReviewerDomain.create({ + reviewId, + reviewerId: reviewerUserId, + fullName: 'Sara Lopez', + positionId, + positionTitle: 'Engineer', + }), + ); + + const result = await repo.listByReview(reviewId, { + fullName: 'SARA', + } as any); + + expect(result).toHaveLength(1); + }); + + it('honours sort direction', async () => { + await repo.create( + ReviewerDomain.create({ + reviewId, + reviewerId: reviewerUserId, + fullName: 'Aa Person', + positionId, + positionTitle: 'Engineer', + }), + ); + + const second = await identityUsers.create({ + firstName: 'Zz', + lastName: 'Person', + email: `second.${Date.now()}@example.com`, + } as any); + await repo.create( + ReviewerDomain.create({ + reviewId, + reviewerId: second.id!, + fullName: 'Zz Person', + positionId, + positionTitle: 'Engineer', + }), + ); + + const result = await repo.listByReview(reviewId, { + sortBy: ReviewerSortField.FULL_NAME, + sortDirection: SortDirection.DESC, + } as any); + + expect(result.map((r) => r.fullName)).toEqual([ + 'Zz Person', + 'Aa Person', + ]); + }); + }); + + describe('deleteById', () => { + it('removes the reviewer row', async () => { + const created = await repo.create( + ReviewerDomain.create({ + reviewId, + reviewerId: reviewerUserId, + fullName: 'Sara Lopez', + positionId, + positionTitle: 'Engineer', + }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.reviewer.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/service/cluster-score-analytics.service.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/service/cluster-score-analytics.service.integration-spec.ts new file mode 100644 index 00000000..61900f24 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/service/cluster-score-analytics.service.integration-spec.ts @@ -0,0 +1,291 @@ +import '../../../setup-env'; + +import { ReviewStage } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { ClusterScoreAnalyticsService } from 'src/contexts/feedback360/application/services/cluster-score-analytics.service'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { ClusterScoreAnalyticsDomain } from 'src/contexts/feedback360/domain/cluster-score-analytics.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { ClusterService } from 'src/contexts/library/application/services/cluster.service'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ClusterScoreAnalyticsService (integration)', () => { + let module: TestingModule; + let service: ClusterScoreAnalyticsService; + let cycles: CycleService; + let reviews: ReviewService; + let reviewRepo: ReviewRepository; + let competences: CompetenceService; + let clusters: ClusterService; + let positions: PositionService; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let cycleId: number; + let clusterId: number; + let reviewId: number; + let rateeId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + service = module.get(ClusterScoreAnalyticsService); + cycles = module.get(CycleService); + reviews = module.get(ReviewService); + reviewRepo = module.get(ReviewRepository); + competences = module.get(CompetenceService); + clusters = module.get(ClusterService); + positions = module.get(PositionService); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positions.create({ title: 'Engineer' }); + const cycle = await cycles.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + } as any); + const competence = await competences.create({ title: 'Teamwork' }); + const cluster = await clusters.create({ + competenceId: competence.id!, + lowerBound: 0, + upperBound: 5, + title: 'Beginner', + description: 'starting', + } as any); + // Reviews directly through repo so we don't have to drive the + // full lifecycle just to attach cluster scores. + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + + cycleId = cycle.id!; + clusterId = cluster.id!; + reviewId = review.id!; + rateeId = ratee.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildPayload( + overrides: Partial<{ + minScore: number; + maxScore: number; + averageScore: number; + lowerBound: number; + upperBound: number; + }> = {}, + ): any { + return { + cycleId, + clusterId, + employeesCount: 5, + employeeDensity: 0.5, + lowerBound: overrides.lowerBound ?? 0, + upperBound: overrides.upperBound ?? 5, + minScore: overrides.minScore ?? 1, + maxScore: overrides.maxScore ?? 5, + averageScore: overrides.averageScore ?? 3, + }; + } + + describe('upsert', () => { + it('creates an analytics row when none exists for the (cycle, cluster)', async () => { + const created = await service.upsert(buildPayload()); + + expect(created.id).toBeDefined(); + expect(created.averageScore.toNumber()).toBe(3); + }); + + it('updates the existing row on the unique (cycleId, clusterId) pair', async () => { + const first = await service.upsert(buildPayload()); + const second = await service.upsert( + buildPayload({ averageScore: 4 }), + ); + + expect(second.id).toBe(first.id); + expect(second.averageScore.toNumber()).toBe(4); + }); + + it('throws NotFoundException when the cycle does not exist', async () => { + await expect( + service.upsert({ + ...buildPayload(), + cycleId: 999999, + }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('rejects bounds inverted (lower > upper)', async () => { + await expect( + service.upsert(buildPayload({ lowerBound: 4, upperBound: 1 })), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('rejects scores where min > max', async () => { + await expect( + service.upsert(buildPayload({ minScore: 5, maxScore: 1 })), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('rejects scores where avg lies outside [min, max]', async () => { + await expect( + service.upsert( + buildPayload({ + minScore: 1, + maxScore: 3, + averageScore: 4.5, + }), + ), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('getById / search / getByCycleId', () => { + it('getById returns the row when found', async () => { + const created = await service.upsert(buildPayload()); + + await expect(service.getById(created.id!)).resolves.toBeInstanceOf( + ClusterScoreAnalyticsDomain, + ); + }); + + it('getById throws NotFoundException when missing', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('search filters by cycleId', async () => { + await service.upsert(buildPayload()); + + const result = await service.search({ cycleId } as any); + expect(result).toHaveLength(1); + }); + + it('getByCycleId returns analytics tied to a cycle', async () => { + await service.upsert(buildPayload()); + + const result = await service.getByCycleId(cycleId); + expect(result).toHaveLength(1); + }); + }); + + describe('update', () => { + it('persists patched fields and re-runs score validation', async () => { + const created = await service.upsert(buildPayload()); + + const updated = await service.update(created.id!, { + employeesCount: 10, + } as any); + + expect(updated.employeesCount.toNumber()).toBe(10); + }); + + it('rejects a patch that breaks the min/avg/max invariant', async () => { + const created = await service.upsert(buildPayload()); + + await expect( + service.update(created.id!, { + averageScore: 100, + } as any), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('delete', () => { + it('removes the analytics row', async () => { + const created = await service.upsert(buildPayload()); + + await service.delete(created.id!); + + await expect(service.getById(created.id!)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('generateAnalyticsForCycle', () => { + it('aggregates cluster scores into analytics for every cluster with data', async () => { + // Seed two cluster scores under the same cluster for different ratees. + const secondRatee = await identityUsers.create({ + firstName: 'Alice', + lastName: 'Stone', + email: `alice.${Date.now()}.${Math.random()}@example.com`, + } as any); + + await reviews.upsertClusterScore({ + cycleId, + clusterId, + rateeId, + reviewId, + score: 2, + answersCount: 3, + } as any); + await reviews.upsertClusterScore({ + cycleId, + clusterId, + rateeId: secondRatee.id!, + reviewId, + score: 4, + answersCount: 3, + } as any); + + await service.generateAnalyticsForCycle(cycleId); + + const result = await service.getByCycleId(cycleId); + expect(result).toHaveLength(1); + expect(result[0].minScore.toNumber()).toBe(2); + expect(result[0].maxScore.toNumber()).toBe(4); + expect(result[0].averageScore.toNumber()).toBe(3); + expect(result[0].employeesCount.toNumber()).toBe(2); + }); + + it('is a no-op when the cycle has no cluster scores', async () => { + await service.generateAnalyticsForCycle(cycleId); + + const result = await service.getByCycleId(cycleId); + expect(result).toEqual([]); + }); + + it('throws NotFoundException when the cycle does not exist', async () => { + await expect( + service.generateAnalyticsForCycle(999999), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/service/cycle.service.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/service/cycle.service.integration-spec.ts new file mode 100644 index 00000000..84490b34 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/service/cycle.service.integration-spec.ts @@ -0,0 +1,291 @@ +import '../../../setup-env'; + +import { CycleStage, ReviewStage, SYSTEM_ACTOR } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('CycleService (integration)', () => { + let module: TestingModule; + let service: CycleService; + let reviews: ReviewRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let hrId: number; + let positionId: number; + let rateeId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + service = module.get(CycleService); + reviews = module.get(ReviewRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function basePayload(overrides: Partial<{ title: string }> = {}) { + return { + title: overrides.title ?? 'Q1 2026', + description: 'A quarterly review cycle', + hrId, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + } as any; + } + + describe('create', () => { + it('persists a cycle with stage NEW by default', async () => { + const created = await service.create(basePayload()); + + expect(created.id).toBeDefined(); + expect(created.stage).toBe(CycleStage.NEW); + expect(created.isActive).toBe(true); + }); + }); + + describe('search / getById', () => { + it('search returns persisted cycles', async () => { + await service.create(basePayload({ title: 'Alpha' })); + await service.create(basePayload({ title: 'Beta' })); + + const result = await service.search({} as any); + + expect(result).toHaveLength(2); + }); + + it('getById throws NotFoundException when missing', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('persists patched scalar fields', async () => { + const created = await service.create(basePayload()); + + const updated = await service.update(created.id!, { + description: 'updated', + } as any); + + expect(updated.description).toBe('updated'); + }); + + it('throws NotFoundException when the cycle is missing', async () => { + await expect( + service.update(999999, { description: 'x' } as any), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('transitions the stage via changeStage and writes history', async () => { + const created = await service.create(basePayload()); + + await service.update(created.id!, { + stage: CycleStage.ACTIVE, + } as any); + + const fromDb = await prisma.cycle.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.stage).toBe('ACTIVE'); + + const history = await service.getStageHistory(created.id!); + expect(history).toHaveLength(1); + expect(history[0].fromStage).toBe(CycleStage.NEW); + expect(history[0].toStage).toBe(CycleStage.ACTIVE); + }); + + it('rejects finishing a cycle while reviews are still in progress', async () => { + const created = await service.create(basePayload()); + await service.update(created.id!, { + stage: CycleStage.ACTIVE, + } as any); + + await reviews.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId: created.id!, + stage: ReviewStage.IN_PROGRESS, + }), + ); + + await expect( + service.update(created.id!, { + stage: CycleStage.FINISHED, + } as any), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('changeStage', () => { + it('rejects an invalid transition', async () => { + const created = await service.create(basePayload()); + + await expect( + service.changeStage(created.id!, CycleStage.PUBLISHED), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('persists the stage and emits a history record on a valid transition', async () => { + const created = await service.create(basePayload()); + + await service.changeStage( + created.id!, + CycleStage.ACTIVE, + SYSTEM_ACTOR.ID, + SYSTEM_ACTOR.NAME, + 'kicked off', + ); + + const reloaded = await service.getById(created.id!); + expect(reloaded.stage).toBe(CycleStage.ACTIVE); + + const history = await service.getStageHistory(created.id!); + expect(history).toHaveLength(1); + expect(history[0].reason).toBe('kicked off'); + }); + }); + + describe('completeCycle', () => { + it('transitions an ACTIVE cycle to FINISHED when all reviews are complete', async () => { + const created = await service.create(basePayload()); + await service.changeStage(created.id!, CycleStage.ACTIVE); + + await reviews.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId: created.id!, + stage: ReviewStage.FINISHED, + }), + ); + + await service.completeCycle(created.id!); + + const reloaded = await service.getById(created.id!); + expect(reloaded.stage).toBe(CycleStage.FINISHED); + }); + + it('throws BadRequestException when reviews are still in progress', async () => { + const created = await service.create(basePayload()); + await service.changeStage(created.id!, CycleStage.ACTIVE); + + await reviews.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId: created.id!, + stage: ReviewStage.IN_PROGRESS, + }), + ); + + await expect( + service.completeCycle(created.id!), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('is a no-op when the cycle has no reviews at all', async () => { + const created = await service.create(basePayload()); + await service.changeStage(created.id!, CycleStage.ACTIVE); + + await service.completeCycle(created.id!); + + const reloaded = await service.getById(created.id!); + expect(reloaded.stage).toBe(CycleStage.ACTIVE); + }); + }); + + describe('forceFinish', () => { + it('transitions ACTIVE -> FINISHED regardless of in-progress reviews', async () => { + const created = await service.create(basePayload()); + await service.changeStage(created.id!, CycleStage.ACTIVE); + + await reviews.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId: created.id!, + stage: ReviewStage.IN_PROGRESS, + }), + ); + + await service.forceFinish(created.id!); + + const reloaded = await service.getById(created.id!); + expect(reloaded.stage).toBe(CycleStage.FINISHED); + }); + }); + + describe('delete', () => { + it('removes the cycle row', async () => { + const created = await service.create(basePayload()); + + await service.delete(created.id!); + + const fromDb = await prisma.cycle.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('throws NotFoundException for a missing cycle', async () => { + await expect(service.delete(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/service/review-scheduler.service.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/service/review-scheduler.service.integration-spec.ts new file mode 100644 index 00000000..dd6d043b --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/service/review-scheduler.service.integration-spec.ts @@ -0,0 +1,194 @@ +import '../../../setup-env'; + +import { CycleStage, ReviewStage } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewSchedulerService } from 'src/contexts/feedback360/application/services/review-scheduler.service'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ReviewSchedulerService (integration)', () => { + let module: TestingModule; + let scheduler: ReviewSchedulerService; + let reviewRepo: ReviewRepository; + let cycleRepo: CycleRepository; + let cycles: CycleService; + let reviews: ReviewService; + let identityUsers: IdentityUserService; + let positions: PositionService; + let prisma: PrismaService; + + let hrId: number; + let rateeId: number; + let positionId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + scheduler = module.get(ReviewSchedulerService); + reviewRepo = module.get(ReviewRepository); + cycleRepo = module.get(CycleRepository); + cycles = module.get(CycleService); + reviews = module.get(ReviewService); + identityUsers = module.get(IdentityUserService); + positions = module.get(PositionService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positions.create({ title: 'Engineer' }); + + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function createCycle( + overrides: Partial<{ + stage: CycleStage; + responseDeadline: Date | null; + endDate: Date; + }> = {}, + ): Promise { + // The CycleService.create() path doesn't accept `stage`, so insert + // through the repo when the test needs a non-NEW cycle. + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId, + stage: overrides.stage ?? CycleStage.ACTIVE, + isActive: true, + startDate: new Date('2026-01-01'), + endDate: overrides.endDate ?? new Date('2027-01-01'), + responseDeadline: + overrides.responseDeadline === undefined + ? null + : overrides.responseDeadline, + }), + ); + return cycle.id!; + } + + async function createReview( + cycleId: number, + stage: ReviewStage = ReviewStage.IN_PROGRESS, + ): Promise { + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId, + stage, + }), + ); + return review.id!; + } + + describe('checkReviewDeadlines', () => { + it('transitions IN_PROGRESS reviews to FINISHED when responseDeadline has passed', async () => { + const cycleId = await createCycle({ + responseDeadline: new Date('2020-01-01'), // in the past + }); + const reviewId = await createReview(cycleId); + + await scheduler.checkReviewDeadlines(); + + const reloaded = await reviews.getById(reviewId); + expect(reloaded.stage).toBe(ReviewStage.FINISHED); + + const history = await reviews.getStageHistory(reviewId); + expect(history).toHaveLength(1); + expect(history[0].toStage).toBe(ReviewStage.FINISHED); + }); + + it('does nothing when the responseDeadline is in the future', async () => { + const cycleId = await createCycle({ + responseDeadline: new Date('2099-01-01'), // far future + }); + const reviewId = await createReview(cycleId); + + await scheduler.checkReviewDeadlines(); + + const reloaded = await reviews.getById(reviewId); + expect(reloaded.stage).toBe(ReviewStage.IN_PROGRESS); + }); + + it('skips cycles without a responseDeadline', async () => { + const cycleId = await createCycle({ responseDeadline: null }); + const reviewId = await createReview(cycleId); + + await scheduler.checkReviewDeadlines(); + + const reloaded = await reviews.getById(reviewId); + expect(reloaded.stage).toBe(ReviewStage.IN_PROGRESS); + }); + + it('only affects reviews in IN_PROGRESS stage', async () => { + const cycleId = await createCycle({ + responseDeadline: new Date('2020-01-01'), + }); + // A NEW review under the same overdue cycle should not be touched. + const newReviewId = await createReview(cycleId, ReviewStage.NEW); + + await scheduler.checkReviewDeadlines(); + + const reloaded = await reviews.getById(newReviewId); + expect(reloaded.stage).toBe(ReviewStage.NEW); + }); + }); + + describe('checkCycleDeadlines', () => { + it('force-finishes an ACTIVE cycle whose endDate has passed', async () => { + const cycleId = await createCycle({ + stage: CycleStage.ACTIVE, + endDate: new Date('2020-01-01'), + }); + + await scheduler.checkCycleDeadlines(); + + const reloaded = await cycles.getById(cycleId); + expect(reloaded.stage).toBe(CycleStage.FINISHED); + }); + + it('does nothing when both endDate and responseDeadline are in the future', async () => { + const cycleId = await createCycle({ + stage: CycleStage.ACTIVE, + endDate: new Date('2099-01-01'), + responseDeadline: new Date('2099-01-01'), + }); + + await scheduler.checkCycleDeadlines(); + + const reloaded = await cycles.getById(cycleId); + expect(reloaded.stage).toBe(CycleStage.ACTIVE); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/service/review.service.integration-spec.ts b/apps/api/test/integration/contexts/feedback360/service/review.service.integration-spec.ts new file mode 100644 index 00000000..63afcd21 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/service/review.service.integration-spec.ts @@ -0,0 +1,681 @@ +import '../../../setup-env'; + +import { + AnswerType, + CycleStage, + IdentityRole, + RespondentCategory, + ResponseStatus, + ReviewStage, + SYSTEM_ACTOR, +} from '@intra/shared-kernel'; +import { + BadRequestException, + ForbiddenException, + NotFoundException, +} from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createFeedback360TestModule, + resetFeedback360Tables, +} from '../test-app-feedback360'; + +describe('ReviewService (integration)', () => { + let module: TestingModule; + let service: ReviewService; + let cycles: CycleService; + let competences: CompetenceService; + let questionTemplates: QuestionTemplateService; + let positions: PositionService; + let identityUsers: IdentityUserService; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let prisma: PrismaService; + + let hrId: number; + let rateeId: number; + let positionId: number; + let cycleId: number; + let competenceId: number; + let questionTemplateId: number; + + beforeAll(async () => { + module = await createFeedback360TestModule(); + service = module.get(ReviewService); + cycles = module.get(CycleService); + competences = module.get(CompetenceService); + questionTemplates = module.get(QuestionTemplateService); + positions = module.get(PositionService); + identityUsers = module.get(IdentityUserService); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetFeedback360Tables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positions.create({ title: 'Engineer' }); + const cycle = await cycles.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + } as any); + const competence = await competences.create({ title: 'Teamwork' }); + const template = await questionTemplates.create({ + competenceId: competence.id!, + title: 'Rate teamwork', + answerType: AnswerType.NUMERICAL_SCALE, + } as any); + + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + cycleId = cycle.id!; + competenceId = competence.id!; + questionTemplateId = template.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function basePayload( + overrides: Partial<{ cycleId: number | null }> = {}, + ): any { + return { + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId: + overrides.cycleId === undefined ? cycleId : overrides.cycleId, + }; + } + + async function createReviewAt(stage: ReviewStage): Promise { + // Insert directly through the repository to bypass stage-machine + // validation when we want to start at a specific stage. + const review = await reviewRepo.create( + ReviewDomain.create({ + ...basePayload(), + stage, + } as any), + ); + return review.id!; + } + + async function newUserId(seed: string): Promise { + const user = await identityUsers.create({ + firstName: seed, + lastName: 'Person', + email: `${seed}.${Date.now()}.${Math.random()}@example.com`, + } as any); + return user.id!; + } + + describe('create', () => { + it('persists a review with NEW stage by default', async () => { + const created = await service.create(basePayload()); + + expect(created.id).toBeDefined(); + expect(created.stage).toBe(ReviewStage.NEW); + expect(created.rateeId).toBe(rateeId); + + const fromDb = await prisma.review.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.rateeFullName).toBe('Robert Smith'); + }); + + it('throws NotFoundException when cycleId references a missing cycle', async () => { + await expect( + service.create(basePayload({ cycleId: 999999 })), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('search / getById / RBAC', () => { + it('getById throws NotFoundException for a missing review', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('search without actor returns nothing (no access)', async () => { + await service.create(basePayload()); + + const result = await service.search({} as any); + + expect(result).toEqual([]); + }); + + it('search with HR actor returns all reviews', async () => { + await service.create(basePayload()); + const hrActor = UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + + const result = await service.search({} as any, hrActor); + + expect(result).toHaveLength(1); + }); + + it('getById with ratee actor succeeds', async () => { + const review = await service.create(basePayload()); + const rateeActor = UserDomain.create({ + id: rateeId, + firstName: 'Robert', + lastName: 'Smith', + email: 'ratee@example.com', + }); + + await expect( + service.getById(review.id!, rateeActor), + ).resolves.toBeDefined(); + }); + + it('getById with an unrelated actor throws ForbiddenException', async () => { + const review = await service.create(basePayload()); + const otherUserId = await newUserId('Other'); + const otherActor = UserDomain.create({ + id: otherUserId, + firstName: 'Other', + lastName: 'Person', + email: 'other@example.com', + }); + + await expect( + service.getById(review.id!, otherActor), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('update', () => { + it('persists patched fields when the review is NEW', async () => { + const review = await service.create(basePayload()); + const hrActor = UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + + const updated = await service.update( + review.id!, + { hrNote: 'updated' } as any, + hrActor, + ); + + expect(updated.hrNote).toBe('updated'); + }); + + it('rejects updates when the review has progressed past PROCESSING_BY_HR', async () => { + const reviewId = await createReviewAt(ReviewStage.IN_PROGRESS); + const hrActor = UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + + await expect( + service.update(reviewId, { hrNote: 'updated' } as any, hrActor), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('delete', () => { + it('removes the review row and its dependent rows', async () => { + const review = await service.create(basePayload()); + + await service.delete(review.id!); + + const fromDb = await prisma.review.findUnique({ + where: { id: review.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('rejects deletion when answers are already submitted', async () => { + const reviewId = await createReviewAt(ReviewStage.IN_PROGRESS); + const question = await service.createQuestion({ + cycleId, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId, + } as any); + + await prisma.answer.create({ + data: { + reviewId, + questionId: question.id!, + respondentCategory: 'TEAM' as any, + answerType: 'NUMERICAL_SCALE' as any, + numericalValue: 4, + }, + }); + + await expect(service.delete(reviewId)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + }); + + describe('createQuestion / deleteQuestion', () => { + it('persists a question without requiring a template', async () => { + const created = await service.createQuestion({ + cycleId, + title: 'Open question', + answerType: AnswerType.TEXT_FIELD, + } as any); + + expect(created.id).toBeDefined(); + expect(created.title).toBe('Open question'); + }); + + it('inherits title / answerType from the template when not specified', async () => { + const created = await service.createQuestion({ + cycleId, + questionTemplateId, + } as any); + + expect(created.title).toBe('Rate teamwork'); + expect(created.answerType).toBe(AnswerType.NUMERICAL_SCALE); + }); + + it('deleteQuestion removes the row when the cycle is still active', async () => { + const created = await service.createQuestion({ + cycleId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + + await service.deleteQuestion(created.id!); + + const fromDb = await prisma.question.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('deleteQuestion rejects when the parent cycle is finished', async () => { + const created = await service.createQuestion({ + cycleId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + // Force the cycle past ACTIVE by writing it directly. + await cycleRepo.updateById(cycleId, { + stage: CycleStage.FINISHED, + } as any); + + await expect( + service.deleteQuestion(created.id!), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('attachQuestion / detachQuestion / listQuestionRelations', () => { + it('attaches a question template to a NEW review and creates the relation', async () => { + const review = await service.create(basePayload()); + + const relation = await service.attachQuestion({ + reviewId: review.id!, + questionTemplateId, + } as any); + + expect(relation.reviewId).toBe(review.id); + expect(relation.questionTitle).toBe('Rate teamwork'); + + const all = await service.listQuestionRelations( + review.id!, + {} as any, + ); + expect(all).toHaveLength(1); + }); + + it('rejects attaching a question once the review has started', async () => { + const reviewId = await createReviewAt(ReviewStage.IN_PROGRESS); + + await expect( + service.attachQuestion({ + reviewId, + questionTemplateId, + } as any), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('detachQuestion removes the relation while the review is NEW', async () => { + const review = await service.create(basePayload()); + const relation = await service.attachQuestion({ + reviewId: review.id!, + questionTemplateId, + } as any); + + await service.detachQuestion(review.id!, relation.questionId); + + const remaining = await service.listQuestionRelations( + review.id!, + {} as any, + ); + expect(remaining).toEqual([]); + }); + }); + + describe('addRespondent / changeRespondentStatus / completeReview', () => { + async function addNewRespondent( + reviewId: number, + respondentUserId: number, + fullName: string, + ) { + return service.addRespondent({ + reviewId, + respondentId: respondentUserId, + fullName, + category: RespondentCategory.TEAM, + responseStatus: ResponseStatus.PENDING, + positionId, + positionTitle: 'Engineer', + } as any); + } + + it('persists a respondent and the row appears in listRespondents', async () => { + const review = await service.create(basePayload()); + const userId = await newUserId('Sara'); + + await addNewRespondent(review.id!, userId, 'Sara Lopez'); + + const all = await service.listRespondents( + review.id!, + {} as any, + UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }), + ); + expect(all).toHaveLength(1); + expect(all[0].fullName).toBe('Sara Lopez'); + }); + + it('rejects adding a respondent when the review is finished', async () => { + const reviewId = await createReviewAt(ReviewStage.FINISHED); + const userId = await newUserId('Sara'); + + await expect( + addNewRespondent(reviewId, userId, 'Sara Lopez'), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('changeRespondentStatus validates the transition', async () => { + const reviewId = await createReviewAt(ReviewStage.IN_PROGRESS); + const userId = await newUserId('Sara'); + const respondent = await addNewRespondent( + reviewId, + userId, + 'Sara Lopez', + ); + + await expect( + service.changeRespondentStatus( + reviewId, + respondent.id!, + ResponseStatus.COMPLETED, + ), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('completeReview transitions IN_PROGRESS → FINISHED when all respondents are COMPLETED', async () => { + const reviewId = await createReviewAt(ReviewStage.IN_PROGRESS); + const userId = await newUserId('Sara'); + const respondent = await addNewRespondent( + reviewId, + userId, + 'Sara Lopez', + ); + + await service.changeRespondentStatus( + reviewId, + respondent.id!, + ResponseStatus.IN_PROGRESS, + ); + await service.changeRespondentStatus( + reviewId, + respondent.id!, + ResponseStatus.COMPLETED, + ); + + await service.completeReview(reviewId); + + const reloaded = await service.getById(reviewId); + expect(reloaded.stage).toBe(ReviewStage.FINISHED); + }); + + it('completeReview rejects when at least one respondent is still PENDING', async () => { + const reviewId = await createReviewAt(ReviewStage.IN_PROGRESS); + const userId = await newUserId('Sara'); + await addNewRespondent(reviewId, userId, 'Sara Lopez'); + + await expect( + service.completeReview(reviewId), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('addReviewer / removeReviewer', () => { + it('persists a reviewer row attached to the review', async () => { + const review = await service.create(basePayload()); + const userId = await newUserId('Reviewer'); + + const reviewer = await service.addReviewer({ + reviewId: review.id!, + reviewerId: userId, + fullName: 'Reviewer Person', + positionId, + positionTitle: 'Engineer', + } as any); + + expect(reviewer.id).toBeDefined(); + const fromDb = await prisma.reviewer.findMany({ + where: { reviewId: review.id! }, + }); + expect(fromDb).toHaveLength(1); + }); + + it('removeReviewer drops the row', async () => { + const review = await service.create(basePayload()); + const userId = await newUserId('Reviewer'); + const reviewer = await service.addReviewer({ + reviewId: review.id!, + reviewerId: userId, + fullName: 'Reviewer Person', + positionId, + positionTitle: 'Engineer', + } as any); + + await service.removeReviewer(reviewer.id!); + + const remaining = await prisma.reviewer.findMany({ + where: { reviewId: review.id! }, + }); + expect(remaining).toEqual([]); + }); + }); + + describe('addAnswer', () => { + it('persists an answer when the review is IN_PROGRESS', async () => { + const reviewId = await createReviewAt(ReviewStage.IN_PROGRESS); + const question = await service.createQuestion({ + cycleId, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + } as any); + const hrActor = UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + + const answer = await service.addAnswer( + { + reviewId, + questionId: question.id!, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + } as any, + hrActor, + ); + + expect(answer.id).toBeDefined(); + expect(answer.numericalValue).toBe(4); + }); + + it('rejects answers when the review is still NEW', async () => { + const review = await service.create(basePayload()); + const question = await service.createQuestion({ + cycleId, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + } as any); + const hrActor = UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + + await expect( + service.addAnswer( + { + reviewId: review.id!, + questionId: question.id!, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + } as any, + hrActor, + ), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('upsertClusterScore / listClusterScores', () => { + async function makeCluster() { + const cluster = await prisma.cluster.create({ + data: { + competenceId, + lowerBound: 0, + upperBound: 5, + title: 'Beginner', + description: '', + }, + }); + return cluster.id; + } + + it('creates and then updates a cluster score for the same (cluster, ratee) pair', async () => { + const reviewId = await createReviewAt(ReviewStage.FINISHED); + const clusterId = await makeCluster(); + + const first = await service.upsertClusterScore({ + cycleId, + clusterId, + rateeId, + reviewId, + score: 3.5, + answersCount: 5, + } as any); + const second = await service.upsertClusterScore({ + cycleId, + clusterId, + rateeId, + reviewId, + score: 4.1, + answersCount: 6, + } as any); + + expect(second.id).toBe(first.id); + expect(second.score.toNumber()).toBe(4.1); + + const all = await service.listClusterScores({ + cycleId, + } as any); + expect(all).toHaveLength(1); + }); + }); + + describe('changeReviewStage / getStageHistory / forceFinishReview', () => { + it('rejects an invalid transition', async () => { + const review = await service.create(basePayload()); + + await expect( + service.changeReviewStage(review.id!, ReviewStage.PUBLISHED), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('valid transition persists the new stage and writes a history record', async () => { + const review = await service.create(basePayload()); + + await service.changeReviewStage( + review.id!, + ReviewStage.SELF_ASSESSMENT, + SYSTEM_ACTOR.ID, + SYSTEM_ACTOR.NAME, + 'kicked off', + ); + + const reloaded = await service.getById(review.id!); + expect(reloaded.stage).toBe(ReviewStage.SELF_ASSESSMENT); + + const history = await service.getStageHistory(review.id!); + expect(history).toHaveLength(1); + expect(history[0].toStage).toBe(ReviewStage.SELF_ASSESSMENT); + expect(history[0].reason).toBe('kicked off'); + }); + + it('forceFinishReview moves an IN_PROGRESS review to FINISHED', async () => { + const reviewId = await createReviewAt(ReviewStage.IN_PROGRESS); + + await service.forceFinishReview(reviewId, hrId, 'Helena Reed'); + + const reloaded = await service.getById(reviewId); + expect(reloaded.stage).toBe(ReviewStage.FINISHED); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/feedback360/test-app-feedback360.ts b/apps/api/test/integration/contexts/feedback360/test-app-feedback360.ts new file mode 100644 index 00000000..bcc24474 --- /dev/null +++ b/apps/api/test/integration/contexts/feedback360/test-app-feedback360.ts @@ -0,0 +1,267 @@ +import { ConfigModule } from '@nestjs/config'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { Test, TestingModule } from '@nestjs/testing'; +import appConfig from 'src/config/app'; +import databaseConfig from 'src/config/database'; +import { CycleStageListener } from 'src/contexts/feedback360/application/listeners/cycle-stage.listener'; +import { + RespondentStatusListener, + SelfAssessmentCompletedListener, +} from 'src/contexts/feedback360/application/listeners/respondent-status.listener'; +import { ReviewStageListener } from 'src/contexts/feedback360/application/listeners/review-stage.listener'; +import { ANSWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/answer.repository.port'; +import { CLUSTER_SCORE_ANALYTICS_REPOSITORY } from 'src/contexts/feedback360/application/ports/cluster-score-analytics.repository.port'; +import { CLUSTER_SCORE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cluster-score.repository.port'; +import { CYCLE_STAGE_HISTORY_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle-stage-history.repository.port'; +import { CYCLE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle.repository.port'; +import { QUESTION_REPOSITORY } from 'src/contexts/feedback360/application/ports/question.repository.port'; +import { RESPONDENT_REPOSITORY } from 'src/contexts/feedback360/application/ports/respondent.repository.port'; +import { REVIEW_QUESTION_RELATION_REPOSITORY } from 'src/contexts/feedback360/application/ports/review-question-relation.repository.port'; +import { REVIEW_STAGE_HISTORY_REPOSITORY } from 'src/contexts/feedback360/application/ports/review-stage-history.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { REVIEWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/reviewer.repository.port'; +import { ClusterScoreAnalyticsService } from 'src/contexts/feedback360/application/services/cluster-score-analytics.service'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewSchedulerService } from 'src/contexts/feedback360/application/services/review-scheduler.service'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { AnswerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository'; +import { ClusterScoreAnalyticsRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository'; +import { ClusterScoreRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score.repository'; +import { CycleStageHistoryRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { RespondentRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository'; +import { ReviewQuestionRelationRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review-question-relation.repository'; +import { ReviewStageHistoryRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review-stage-history.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { ReviewerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/reviewer.repository'; +import { IDENTITY_ROLE_REPOSITORY } from 'src/contexts/identity/application/ports/role.repository.port'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { RoleRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/role.repository'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { CLUSTER_REPOSITORY } from 'src/contexts/library/application/ports/cluster.repository.port'; +import { COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/competence-question-template-relation.repository.port'; +import { COMPETENCE_REPOSITORY } from 'src/contexts/library/application/ports/competence.repository.port'; +import { POSITION_COMPETENCE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-competence-relation.repository.port'; +import { POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-question-template-relation.repository.port'; +import { QUESTION_TEMPLATE_REPOSITORY } from 'src/contexts/library/application/ports/question-template.repository.port'; +import { ClusterService } from 'src/contexts/library/application/services/cluster.service'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service'; +import { ClusterRepository } from 'src/contexts/library/infrastructure/prisma-repositories/cluster.repository'; +import { CompetenceQuestionTemplateRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence-question-template-relation.repository'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionCompetenceRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-competence-relation.repository'; +import { PositionQuestionTemplateRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-question-template-relation.repository'; +import { QuestionTemplateRepository } from 'src/contexts/library/infrastructure/prisma-repositories/question-template.repository'; +import { ORGANISATION_POSITION_REPOSITORY } from 'src/contexts/organisation/application/ports/position.repository.port'; +import { ORGANISATION_TEAM_REPOSITORY } from 'src/contexts/organisation/application/ports/team.repository.port'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { TeamRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/team.repository'; +import { STRATEGIC_REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report.repository.port'; +import { DatabaseModule } from 'src/database/database.module'; +import { PrismaService } from 'src/database/prisma.service'; +import { ensureTestDatabaseExists } from '../../setup-env'; + +/** + * Bootstraps a Nest testing module for the feedback360 context wired to + * the real database. We assemble the providers manually instead of + * importing `Feedback360Module` because that module pulls in + * `AuthModule`, `LibraryModule`, the reporting context, and a fleet of + * listeners that require the full HTTP / better-auth setup. + * + * Cross-context dependencies wired in: + * - Identity (`UserRepository` + `IdentityUserService`) so reviews/ + * cycles can reference real `identity_users` rows for HR, ratee, + * manager and reviewer ids. + * - Organisation (`PositionRepository`, `TeamRepository`) so reviews + * can reference real positions/teams without FK errors. + * - Reporting `STRATEGIC_REPORT_REPOSITORY` is stubbed — `CycleService` + * only needs `findByCycleId()`, and pulling in the full reporting + * module would expand the surface area for service-level tests + * considerably. + */ +export async function createFeedback360TestModule(): Promise { + await ensureTestDatabaseExists(); + + const strategicReportStub = { + async findByCycleId() { + return null; + }, + async create() { + throw new Error('not implemented in test stub'); + }, + async findById() { + return null; + }, + async search() { + return []; + }, + }; + + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig], + ignoreEnvFile: true, + }), + EventEmitterModule.forRoot(), + DatabaseModule, + ], + providers: [ + // Feedback360 — services + CycleService, + ReviewService, + ReviewSchedulerService, + ClusterScoreAnalyticsService, + // Feedback360 — listeners + CycleStageListener, + ReviewStageListener, + RespondentStatusListener, + SelfAssessmentCompletedListener, + // Feedback360 — repositories + CycleRepository, + ReviewRepository, + QuestionRepository, + ReviewQuestionRelationRepository, + AnswerRepository, + RespondentRepository, + ReviewerRepository, + ClusterScoreRepository, + ClusterScoreAnalyticsRepository, + ReviewStageHistoryRepository, + CycleStageHistoryRepository, + { provide: CYCLE_REPOSITORY, useExisting: CycleRepository }, + { provide: REVIEW_REPOSITORY, useExisting: ReviewRepository }, + { provide: QUESTION_REPOSITORY, useExisting: QuestionRepository }, + { + provide: REVIEW_QUESTION_RELATION_REPOSITORY, + useExisting: ReviewQuestionRelationRepository, + }, + { provide: ANSWER_REPOSITORY, useExisting: AnswerRepository }, + { + provide: RESPONDENT_REPOSITORY, + useExisting: RespondentRepository, + }, + { provide: REVIEWER_REPOSITORY, useExisting: ReviewerRepository }, + { + provide: CLUSTER_SCORE_REPOSITORY, + useExisting: ClusterScoreRepository, + }, + { + provide: CLUSTER_SCORE_ANALYTICS_REPOSITORY, + useExisting: ClusterScoreAnalyticsRepository, + }, + { + provide: REVIEW_STAGE_HISTORY_REPOSITORY, + useExisting: ReviewStageHistoryRepository, + }, + { + provide: CYCLE_STAGE_HISTORY_REPOSITORY, + useExisting: CycleStageHistoryRepository, + }, + // Identity + IdentityUserService, + IdentityRoleService, + UserRepository, + RoleRepository, + { provide: IDENTITY_USER_REPOSITORY, useExisting: UserRepository }, + { provide: IDENTITY_ROLE_REPOSITORY, useExisting: RoleRepository }, + // Library (needed for cluster score / question relation tests + // + ReviewService.attachQuestion and CompetenceService / + // QuestionTemplateService dependencies from ReviewService) + CompetenceService, + ClusterService, + QuestionTemplateService, + CompetenceRepository, + ClusterRepository, + QuestionTemplateRepository, + PositionCompetenceRelationRepository, + PositionQuestionTemplateRelationRepository, + CompetenceQuestionTemplateRelationRepository, + { + provide: COMPETENCE_REPOSITORY, + useExisting: CompetenceRepository, + }, + { provide: CLUSTER_REPOSITORY, useExisting: ClusterRepository }, + { + provide: QUESTION_TEMPLATE_REPOSITORY, + useExisting: QuestionTemplateRepository, + }, + { + provide: POSITION_COMPETENCE_RELATION_REPOSITORY, + useExisting: PositionCompetenceRelationRepository, + }, + { + provide: POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useExisting: PositionQuestionTemplateRelationRepository, + }, + { + provide: COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useExisting: CompetenceQuestionTemplateRelationRepository, + }, + // Organisation + PositionService, + PositionRepository, + TeamRepository, + { + provide: ORGANISATION_POSITION_REPOSITORY, + useExisting: PositionRepository, + }, + { + provide: ORGANISATION_TEAM_REPOSITORY, + useExisting: TeamRepository, + }, + // Reporting — stub + { + provide: STRATEGIC_REPORT_REPOSITORY, + useValue: strategicReportStub, + }, + ], + }).compile(); + + await module.init(); + return module; +} + +/** + * Truncates every table touched by feedback360 scenarios. The order is + * not significant because we use CASCADE, but the explicit list + * documents the schema dependencies. `identity_roles` (reference data) + * is preserved. + */ +export async function resetFeedback360Tables( + prisma: PrismaService, +): Promise { + await prisma.$executeRawUnsafe( + `TRUNCATE TABLE + "feedback360_answers", + "feedback360_cluster_score_analytics", + "feedback360_cluster_scores", + "feedback360_review_question_relations", + "feedback360_review_stage_history", + "feedback360_cycle_stage_history", + "feedback360_respondents", + "feedback360_reviewers", + "feedback360_questions", + "feedback360_reviews", + "feedback360_cycles", + "library_clusters", + "library_position_question_template_relations", + "library_position_competence_relations", + "library_competence_question_template_relations", + "library_question_templates", + "library_competences", + "org_team_memberships", + "org_teams", + "org_position_hierarchies", + "org_positions", + "identity_users_roles", + "identity_users" + RESTART IDENTITY CASCADE`, + ); +} diff --git a/apps/api/test/integration/contexts/identity/repository/role.repository.integration-spec.ts b/apps/api/test/integration/contexts/identity/repository/role.repository.integration-spec.ts new file mode 100644 index 00000000..671f1fc0 --- /dev/null +++ b/apps/api/test/integration/contexts/identity/repository/role.repository.integration-spec.ts @@ -0,0 +1,78 @@ +import '../../../setup-env'; + +import { IdentityRole } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { RoleDomain } from 'src/contexts/identity/domain/role.domain'; +import { RoleRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/role.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createIdentityTestModule, + ensureRolesSeeded, +} from '../test-app-identity'; + +describe('RoleRepository (integration)', () => { + let module: TestingModule; + let repo: RoleRepository; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createIdentityTestModule(); + repo = module.get(RoleRepository); + prisma = module.get(PrismaService); + await ensureRolesSeeded(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('getAll', () => { + it('returns all four seeded identity roles', async () => { + const roles = await repo.getAll(); + + expect(roles.map((r) => r.code).sort()).toEqual( + [ + IdentityRole.ADMIN, + IdentityRole.EMPLOYEE, + IdentityRole.HR, + IdentityRole.MANAGER, + ].sort(), + ); + roles.forEach((r) => expect(r).toBeInstanceOf(RoleDomain)); + }); + + it('returns roles in a stable order across calls', async () => { + // Prisma sorts enum columns in the order their values are + // declared in the schema (ADMIN → HR → MANAGER → EMPLOYEE), + // not lexicographically. We just assert determinism. + const first = (await repo.getAll()).map((r) => r.code); + const second = (await repo.getAll()).map((r) => r.code); + expect(second).toEqual(first); + }); + }); + + describe('findByCodes', () => { + it('returns the roles matching the requested codes', async () => { + const roles = await repo.findByCodes([ + IdentityRole.ADMIN, + IdentityRole.HR, + ]); + + expect(roles).toHaveLength(2); + expect(roles.map((r) => r.code).sort()).toEqual( + [IdentityRole.ADMIN, IdentityRole.HR].sort(), + ); + }); + + it('returns an empty array for an empty codes input', async () => { + await expect(repo.findByCodes([])).resolves.toEqual([]); + }); + + it('returns only roles that actually exist when subset is requested', async () => { + const roles = await repo.findByCodes([IdentityRole.MANAGER]); + + expect(roles).toHaveLength(1); + expect(roles[0].code).toBe(IdentityRole.MANAGER); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/identity/repository/user.repository.integration-spec.ts b/apps/api/test/integration/contexts/identity/repository/user.repository.integration-spec.ts new file mode 100644 index 00000000..5b351152 --- /dev/null +++ b/apps/api/test/integration/contexts/identity/repository/user.repository.integration-spec.ts @@ -0,0 +1,361 @@ +import '../../../setup-env'; + +import { + IdentityRole, + IdentityStatus, + SortDirection, + UserSortField, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createIdentityTestModule, + ensureRolesSeeded, + resetIdentityTables, +} from '../test-app-identity'; + +function buildUserDomain( + overrides: Partial[0]> = {}, +): UserDomain { + return UserDomain.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + fullName: 'Jane Doe', + ...overrides, + }); +} + +describe('UserRepository (integration)', () => { + let module: TestingModule; + let repo: UserRepository; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createIdentityTestModule(); + repo = module.get(UserRepository); + prisma = module.get(PrismaService); + await ensureRolesSeeded(prisma); + }); + + beforeEach(async () => { + await resetIdentityTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a new user with all attributes', async () => { + const created = await repo.create( + buildUserDomain({ + firstName: 'Anna', + secondName: 'M.', + lastName: 'Smith', + fullName: 'Anna M. Smith', + email: 'anna.m.smith@example.com', + status: IdentityStatus.ACTIVE, + }), + ); + + expect(created.id).toBeDefined(); + expect(created.firstName).toBe('Anna'); + expect(created.secondName).toBe('M.'); + + const fromDb = await prisma.user.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).not.toBeNull(); + expect(fromDb!.secondName).toBe('M.'); + }); + + it('persists assigned roles via the userRoles relation', async () => { + const created = await repo.create( + buildUserDomain({ + email: 'roles.test@example.com', + roles: [IdentityRole.HR, IdentityRole.MANAGER], + }), + ); + + const rows = await prisma.userRole.findMany({ + where: { userId: created.id! }, + }); + expect(rows.map((r) => r.roleCode).sort()).toEqual( + ['HR', 'MANAGER'].sort(), + ); + }); + }); + + describe('findById', () => { + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + + it('returns the user without roles when withRoles is not requested', async () => { + const created = await repo.create( + buildUserDomain({ + email: 'find-by-id@example.com', + roles: [IdentityRole.ADMIN], + }), + ); + + const fetched = await repo.findById(created.id!); + expect(fetched).not.toBeNull(); + expect(fetched!.roles).toEqual([]); + }); + + it('returns the user with roles when withRoles is requested', async () => { + const created = await repo.create( + buildUserDomain({ + email: 'find-by-id-roles@example.com', + roles: [IdentityRole.ADMIN, IdentityRole.HR], + }), + ); + + const fetched = await repo.findById(created.id!, { + withRoles: true, + }); + expect(fetched!.roles.sort()).toEqual( + [IdentityRole.ADMIN, IdentityRole.HR].sort(), + ); + }); + }); + + describe('findByEmail', () => { + it('returns the user found by email', async () => { + await repo.create( + buildUserDomain({ email: 'find-by-email@example.com' }), + ); + + const fetched = await repo.findByEmail('find-by-email@example.com'); + expect(fetched?.email).toBe('find-by-email@example.com'); + }); + + it('returns null when no user has the email', async () => { + await expect( + repo.findByEmail('nobody@example.com'), + ).resolves.toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await repo.create( + buildUserDomain({ + firstName: 'Alice', + lastName: 'Brown', + fullName: 'Alice Brown', + email: 'alice@example.com', + }), + ); + await repo.create( + buildUserDomain({ + firstName: 'Bob', + lastName: 'Green', + fullName: 'Bob Green', + email: 'bob@example.com', + status: IdentityStatus.INACTIVE, + }), + ); + await repo.create( + buildUserDomain({ + firstName: 'Carol', + lastName: 'Brown', + fullName: 'Carol Brown', + email: 'carol@example.com', + }), + ); + }); + + it('returns all users with default ordering (ascending by id)', async () => { + const all = await repo.search({} as any); + + expect(all).toHaveLength(3); + const ids = all.map((u) => u.id!); + expect(ids).toEqual([...ids].sort((a, b) => a - b)); + }); + + it('filters by lastName (case insensitive substring)', async () => { + const result = await repo.search({ lastName: 'BROWN' } as any); + + expect(result.map((u) => u.email).sort()).toEqual([ + 'alice@example.com', + 'carol@example.com', + ]); + }); + + it('filters by status', async () => { + const inactive = await repo.search({ + status: IdentityStatus.INACTIVE, + } as any); + + expect(inactive.map((u) => u.email)).toEqual(['bob@example.com']); + }); + + it('filters by the `search` field across firstName / lastName / fullName / email', async () => { + const byEmail = await repo.search({ + search: 'alice@', + } as any); + expect(byEmail.map((u) => u.email)).toEqual(['alice@example.com']); + + const byName = await repo.search({ search: 'Carol' } as any); + expect(byName.map((u) => u.email)).toEqual(['carol@example.com']); + }); + + it('honours descending sort direction on email', async () => { + const result = await repo.search({ + sortBy: UserSortField.EMAIL, + sortDirection: SortDirection.DESC, + } as any); + + const emails = result.map((u) => u.email); + expect(emails).toEqual([...emails].sort().reverse()); + }); + }); + + describe('updateById', () => { + it('updates only the fields supplied in the patch', async () => { + const created = await repo.create( + buildUserDomain({ email: 'update@example.com' }), + ); + + const updated = await repo.updateById(created.id!, { + firstName: 'Updated', + fullName: 'Updated Doe', + } as any); + + expect(updated.firstName).toBe('Updated'); + expect(updated.fullName).toBe('Updated Doe'); + + const fromDb = await prisma.user.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.firstName).toBe('Updated'); + }); + + it('coerces null teamId/managerId/secondName to null in the database', async () => { + const created = await repo.create( + buildUserDomain({ + email: 'null-fields@example.com', + secondName: 'M.', + }), + ); + + await repo.updateById(created.id!, { + secondName: undefined, + teamId: undefined, + managerId: undefined, + } as any); + + const fromDb = await prisma.user.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.secondName).toBeNull(); + expect(fromDb!.teamId).toBeNull(); + expect(fromDb!.managerId).toBeNull(); + }); + }); + + describe('deleteById', () => { + it('removes the user row from the database', async () => { + const created = await repo.create( + buildUserDomain({ email: 'delete@example.com' }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.user.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); + + describe('replaceRoles', () => { + it('inserts new roles when none existed', async () => { + const created = await repo.create( + buildUserDomain({ email: 'roles-insert@example.com' }), + ); + + const result = await repo.replaceRoles(created.id!, [ + IdentityRole.HR, + IdentityRole.ADMIN, + ]); + + expect(result.roles.sort()).toEqual( + [IdentityRole.ADMIN, IdentityRole.HR].sort(), + ); + + const rows = await prisma.userRole.findMany({ + where: { userId: created.id! }, + }); + expect(rows).toHaveLength(2); + }); + + it('removes roles that are no longer in the requested list', async () => { + const created = await repo.create( + buildUserDomain({ + email: 'roles-remove@example.com', + roles: [IdentityRole.HR, IdentityRole.MANAGER], + }), + ); + + const result = await repo.replaceRoles(created.id!, [ + IdentityRole.MANAGER, + ]); + + expect(result.roles).toEqual([IdentityRole.MANAGER]); + const rows = await prisma.userRole.findMany({ + where: { userId: created.id! }, + }); + expect(rows).toHaveLength(1); + expect(rows[0].roleCode).toBe('MANAGER'); + }); + + it('clears all roles when given an empty list', async () => { + const created = await repo.create( + buildUserDomain({ + email: 'roles-clear@example.com', + roles: [IdentityRole.HR], + }), + ); + + const result = await repo.replaceRoles(created.id!, []); + + expect(result.roles).toEqual([]); + const rows = await prisma.userRole.findMany({ + where: { userId: created.id! }, + }); + expect(rows).toEqual([]); + }); + + it('is idempotent when the same role set is reapplied', async () => { + const created = await repo.create( + buildUserDomain({ + email: 'roles-idempotent@example.com', + roles: [IdentityRole.HR, IdentityRole.ADMIN], + }), + ); + + await repo.replaceRoles(created.id!, [ + IdentityRole.HR, + IdentityRole.ADMIN, + ]); + const result = await repo.replaceRoles(created.id!, [ + IdentityRole.HR, + IdentityRole.ADMIN, + ]); + + expect(result.roles.sort()).toEqual( + [IdentityRole.ADMIN, IdentityRole.HR].sort(), + ); + const rows = await prisma.userRole.findMany({ + where: { userId: created.id! }, + }); + expect(rows).toHaveLength(2); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/identity/service/identity-role.service.integration-spec.ts b/apps/api/test/integration/contexts/identity/service/identity-role.service.integration-spec.ts new file mode 100644 index 00000000..22bd822a --- /dev/null +++ b/apps/api/test/integration/contexts/identity/service/identity-role.service.integration-spec.ts @@ -0,0 +1,83 @@ +import '../../../setup-env'; + +import { IdentityRole } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { RoleDomain } from 'src/contexts/identity/domain/role.domain'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createIdentityTestModule, + ensureRolesSeeded, +} from '../test-app-identity'; + +describe('IdentityRoleService (integration)', () => { + let module: TestingModule; + let service: IdentityRoleService; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createIdentityTestModule(); + service = module.get(IdentityRoleService); + prisma = module.get(PrismaService); + await ensureRolesSeeded(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('list', () => { + it('returns every seeded identity role from the database', async () => { + const roles = await service.list(); + + const codes = roles.map((r) => r.code).sort(); + expect(codes).toEqual( + [ + IdentityRole.ADMIN, + IdentityRole.EMPLOYEE, + IdentityRole.HR, + IdentityRole.MANAGER, + ].sort(), + ); + roles.forEach((r) => { + expect(r).toBeInstanceOf(RoleDomain); + expect(r.id).toBeDefined(); + expect(r.title).toBeTruthy(); + }); + }); + + it('returns roles in a deterministic order across calls', async () => { + // Prisma sorts enum columns in the order values are declared + // in the schema, not lexicographically. We just assert the + // order is stable rather than asserting a specific sort. + const first = (await service.list()).map((r) => r.code); + const second = (await service.list()).map((r) => r.code); + expect(second).toEqual(first); + }); + }); + + describe('getByCodes', () => { + it('returns only the roles whose codes are requested', async () => { + const roles = await service.getByCodes([ + IdentityRole.HR, + IdentityRole.MANAGER, + ]); + + expect(roles).toHaveLength(2); + expect(roles.map((r) => r.code).sort()).toEqual( + [IdentityRole.HR, IdentityRole.MANAGER].sort(), + ); + }); + + it('returns an empty array for an empty codes input', async () => { + await expect(service.getByCodes([])).resolves.toEqual([]); + }); + + it('returns whatever subset of requested codes actually exists', async () => { + const roles = await service.getByCodes([IdentityRole.ADMIN]); + + expect(roles).toHaveLength(1); + expect(roles[0].code).toBe(IdentityRole.ADMIN); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/identity/service/identity-user.service.integration-spec.ts b/apps/api/test/integration/contexts/identity/service/identity-user.service.integration-spec.ts new file mode 100644 index 00000000..302c8380 --- /dev/null +++ b/apps/api/test/integration/contexts/identity/service/identity-user.service.integration-spec.ts @@ -0,0 +1,390 @@ +import '../../../setup-env'; + +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TestingModule } from '@nestjs/testing'; +import { UserCreatedEvent } from 'src/contexts/identity/application/events/user-created.event'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createIdentityTestModule, + ensureRolesSeeded, + resetIdentityTables, +} from '../test-app-identity'; + +describe('IdentityUserService (integration)', () => { + let module: TestingModule; + let service: IdentityUserService; + let prisma: PrismaService; + let eventEmitter: EventEmitter2; + + beforeAll(async () => { + module = await createIdentityTestModule(); + service = module.get(IdentityUserService); + prisma = module.get(PrismaService); + eventEmitter = module.get(EventEmitter2); + await ensureRolesSeeded(prisma); + }); + + beforeEach(async () => { + await resetIdentityTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a new user with default status and no roles', async () => { + const created = await service.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + } as any); + + expect(created.id).toBeDefined(); + expect(created.firstName).toBe('Jane'); + expect(created.lastName).toBe('Doe'); + expect(created.fullName).toBe('Doe Jane'); + expect(created.status).toBe(IdentityStatus.ACTIVE); + expect(created.roles).toEqual([]); + + const fromDb = await prisma.user.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).not.toBeNull(); + expect(fromDb!.email).toBe('jane.doe@example.com'); + }); + + it('persists a user together with the requested roles', async () => { + const created = await service.create({ + firstName: 'Anna', + lastName: 'Smith', + email: 'anna.smith@example.com', + roles: [IdentityRole.HR, IdentityRole.MANAGER], + } as any); + + const roleRows = await prisma.userRole.findMany({ + where: { userId: created.id! }, + orderBy: { roleCode: 'asc' }, + }); + expect(roleRows.map((r) => r.roleCode).sort()).toEqual( + ['HR', 'MANAGER'].sort(), + ); + }); + + it('emits a user.created event with the persisted id and email', async () => { + const listener = jest.fn(); + eventEmitter.on('user.created', listener); + + try { + const created = await service.create({ + firstName: 'Bob', + lastName: 'Smith', + email: 'bob.smith@example.com', + } as any); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as UserCreatedEvent; + expect(event.userId).toBe(created.id); + expect(event.email).toBe('bob.smith@example.com'); + } finally { + eventEmitter.off('user.created', listener); + } + }); + + it('rejects a duplicate email with BadRequestException', async () => { + await service.create({ + firstName: 'First', + lastName: 'User', + email: 'duplicate@example.com', + } as any); + + await expect( + service.create({ + firstName: 'Second', + lastName: 'User', + email: 'duplicate@example.com', + } as any), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('honours a supplied fullName instead of rebuilding it', async () => { + const created = await service.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.custom@example.com', + fullName: 'Custom Full Name', + } as any); + + expect(created.fullName).toBe('Custom Full Name'); + }); + }); + + describe('getById / findByEmail / search', () => { + it('returns NotFoundException when the user does not exist', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('returns the user without roles when withRoles is not requested', async () => { + const created = await service.create({ + firstName: 'Carl', + lastName: 'Brown', + email: 'carl.brown@example.com', + roles: [IdentityRole.HR], + } as any); + + const fetched = await service.getById(created.id!); + + expect(fetched.id).toBe(created.id); + expect(fetched.roles).toEqual([]); + }); + + it('returns the user with roles when withRoles is requested', async () => { + const created = await service.create({ + firstName: 'Carl', + lastName: 'Brown', + email: 'carl.brown.roles@example.com', + roles: [IdentityRole.HR, IdentityRole.MANAGER], + } as any); + + const fetched = await service.getById(created.id!, { + withRoles: true, + }); + + expect(fetched.roles.sort()).toEqual( + [IdentityRole.HR, IdentityRole.MANAGER].sort(), + ); + }); + + it('findByEmail returns the user or null', async () => { + await service.create({ + firstName: 'Diana', + lastName: 'Green', + email: 'diana.green@example.com', + } as any); + + const found = await service.findByEmail('diana.green@example.com'); + expect(found?.email).toBe('diana.green@example.com'); + + const missing = await service.findByEmail('nobody@example.com'); + expect(missing).toBeNull(); + }); + + it('search filters by status and matches via the search field', async () => { + await service.create({ + firstName: 'Ed', + lastName: 'Black', + email: 'ed.black@example.com', + } as any); + await service.create({ + firstName: 'Fred', + lastName: 'White', + email: 'fred.white@example.com', + status: IdentityStatus.INACTIVE, + } as any); + + const activeOnly = await service.search({ + status: IdentityStatus.ACTIVE, + } as any); + expect(activeOnly.map((u) => u.email)).toEqual([ + 'ed.black@example.com', + ]); + + const byPartialName = await service.search({ + search: 'Fred', + } as any); + expect(byPartialName.map((u) => u.email)).toEqual([ + 'fred.white@example.com', + ]); + }); + }); + + describe('update', () => { + it('rebuilds fullName when any name part is patched', async () => { + const created = await service.create({ + firstName: 'Old', + lastName: 'Name', + email: 'update@example.com', + } as any); + + const updated = await service.update(created.id!, { + firstName: 'New', + } as any); + + expect(updated.firstName).toBe('New'); + expect(updated.fullName).toBe('Name New'); + }); + + it('preserves fullName when no name field is patched', async () => { + const created = await service.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'update-pos@example.com', + } as any); + + const updated = await service.update(created.id!, { + positionId: null, + teamId: null, + } as any); + + expect(updated.fullName).toBe('Doe Jane'); + }); + + it('throws NotFoundException when updating a missing user', async () => { + await expect( + service.update(999999, { firstName: 'X' } as any), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('replaceRoles', () => { + it('replaces the user role set, removing roles no longer present', async () => { + const created = await service.create({ + firstName: 'Roles', + lastName: 'User', + email: 'roles-user@example.com', + roles: [IdentityRole.HR, IdentityRole.EMPLOYEE], + } as any); + + const updated = await service.replaceRoles(created.id!, [ + IdentityRole.MANAGER, + IdentityRole.EMPLOYEE, + ]); + + expect(updated.roles.sort()).toEqual( + [IdentityRole.MANAGER, IdentityRole.EMPLOYEE].sort(), + ); + + const rows = await prisma.userRole.findMany({ + where: { userId: created.id! }, + orderBy: { roleCode: 'asc' }, + }); + expect(rows.map((r) => r.roleCode).sort()).toEqual( + ['EMPLOYEE', 'MANAGER'].sort(), + ); + }); + + it('clears all roles when given an empty list', async () => { + const created = await service.create({ + firstName: 'Clear', + lastName: 'Roles', + email: 'clear-roles@example.com', + roles: [IdentityRole.HR], + } as any); + + const updated = await service.replaceRoles(created.id!, []); + + expect(updated.roles).toEqual([]); + const rows = await prisma.userRole.findMany({ + where: { userId: created.id! }, + }); + expect(rows).toEqual([]); + }); + + it('deduplicates the requested role list', async () => { + const created = await service.create({ + firstName: 'Dedupe', + lastName: 'Roles', + email: 'dedupe@example.com', + } as any); + + const updated = await service.replaceRoles(created.id!, [ + IdentityRole.HR, + IdentityRole.HR, + IdentityRole.MANAGER, + ]); + + expect(updated.roles.sort()).toEqual( + [IdentityRole.HR, IdentityRole.MANAGER].sort(), + ); + }); + }); + + describe('delete', () => { + it('removes the user row from the database', async () => { + const created = await service.create({ + firstName: 'Doomed', + lastName: 'User', + email: 'doomed@example.com', + } as any); + + await service.delete(created.id!); + + const fromDb = await prisma.user.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('throws NotFoundException when deleting a missing user', async () => { + await expect(service.delete(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('upsertExternalUser', () => { + it('creates a new EMPLOYEE-role user when none exists for the email', async () => { + const user = await service.upsertExternalUser({ + email: 'external@example.com', + firstName: 'Ext', + lastName: 'User', + secondName: undefined, + }); + + expect(user.id).toBeDefined(); + expect(user.roles).toEqual([IdentityRole.EMPLOYEE]); + expect(user.fullName).toBe('User Ext'); + }); + + it('returns the existing user when one with the email is already stored', async () => { + const first = await service.upsertExternalUser({ + email: 'external2@example.com', + firstName: 'Ext2', + lastName: 'User', + secondName: undefined, + }); + + const second = await service.upsertExternalUser({ + email: 'external2@example.com', + firstName: 'Should', + lastName: 'BeIgnored', + secondName: undefined, + }); + + expect(second.id).toBe(first.id); + expect(second.firstName).toBe('Ext2'); + + const rows = await prisma.user.findMany({ + where: { email: 'external2@example.com' }, + }); + expect(rows).toHaveLength(1); + }); + }); + + it('persisted user matches the domain object returned by create', async () => { + const created = await service.create({ + firstName: 'Round', + lastName: 'Trip', + email: 'roundtrip@example.com', + positionId: null, + teamId: null, + managerId: null, + roles: [IdentityRole.ADMIN], + } as any); + + const fetched = await service.getById(created.id!, { + withRoles: true, + }); + + expect(fetched).toBeInstanceOf(UserDomain); + expect(fetched.id).toBe(created.id); + expect(fetched.email).toBe(created.email); + expect(fetched.roles).toEqual([IdentityRole.ADMIN]); + }); +}); diff --git a/apps/api/test/integration/contexts/identity/test-app-identity.ts b/apps/api/test/integration/contexts/identity/test-app-identity.ts new file mode 100644 index 00000000..9fa4163e --- /dev/null +++ b/apps/api/test/integration/contexts/identity/test-app-identity.ts @@ -0,0 +1,90 @@ +import { ConfigModule } from '@nestjs/config'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { Test, TestingModule } from '@nestjs/testing'; +import appConfig from 'src/config/app'; +import databaseConfig from 'src/config/database'; +import { IDENTITY_ROLE_REPOSITORY } from 'src/contexts/identity/application/ports/role.repository.port'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { RoleRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/role.repository'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { DatabaseModule } from 'src/database/database.module'; +import { PrismaService } from 'src/database/prisma.service'; +import { ensureTestDatabaseExists } from '../../setup-env'; + +/** + * Bootstraps a minimal Nest testing module for the identity context + * wired to the real database. We assemble the providers manually instead + * of importing `IdentityModule` because that module pulls in + * `AuthModule` via `forwardRef`, which would require the full HTTP / + * better-auth setup for service-level tests that never need it. + */ +export async function createIdentityTestModule(): Promise { + // Ensure the configured test database exists before Prisma tries to + // open a connection pool against it. Idempotent across spec files + // (the helper caches its promise at module level). + await ensureTestDatabaseExists(); + + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig], + ignoreEnvFile: true, + }), + EventEmitterModule.forRoot(), + DatabaseModule, + ], + providers: [ + IdentityUserService, + IdentityRoleService, + UserRepository, + RoleRepository, + { provide: IDENTITY_USER_REPOSITORY, useExisting: UserRepository }, + { provide: IDENTITY_ROLE_REPOSITORY, useExisting: RoleRepository }, + ], + }).compile(); + + await module.init(); + return module; +} + +/** + * Truncates the user-owned identity tables between tests so every test + * starts from a clean slate, without touching the seeded `identity_roles` + * reference data (which Better-Auth/Prisma migrations populate via the + * `IdentityRole` enum). + */ +export async function resetIdentityTables( + prisma: PrismaService, +): Promise { + await prisma.$executeRawUnsafe( + 'TRUNCATE TABLE "identity_users_roles", "identity_users" RESTART IDENTITY CASCADE', + ); +} + +/** + * Ensures the four reference roles exist in `identity_roles`. Production + * migrations seed them; integration tests run the same `upsert` in case + * the DB was created from a bare schema without the seed. + */ +export async function ensureRolesSeeded(prisma: PrismaService): Promise { + const seed: Array<{ + code: 'ADMIN' | 'HR' | 'MANAGER' | 'EMPLOYEE'; + title: string; + }> = [ + { code: 'ADMIN', title: 'Administrator' }, + { code: 'HR', title: 'HR' }, + { code: 'MANAGER', title: 'Manager' }, + { code: 'EMPLOYEE', title: 'Employee' }, + ]; + + for (const role of seed) { + await prisma.role.upsert({ + where: { code: role.code }, + update: {}, + create: { code: role.code, title: role.title }, + }); + } +} diff --git a/apps/api/test/integration/contexts/library/repository/cluster.repository.integration-spec.ts b/apps/api/test/integration/contexts/library/repository/cluster.repository.integration-spec.ts new file mode 100644 index 00000000..809a97ba --- /dev/null +++ b/apps/api/test/integration/contexts/library/repository/cluster.repository.integration-spec.ts @@ -0,0 +1,167 @@ +import '../../../setup-env'; + +import { ClusterSortField, SortDirection } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { ClusterRepository } from 'src/contexts/library/infrastructure/prisma-repositories/cluster.repository'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('ClusterRepository (integration)', () => { + let module: TestingModule; + let repo: ClusterRepository; + let competences: CompetenceRepository; + let prisma: PrismaService; + let competenceId: number; + + beforeAll(async () => { + module = await createLibraryTestModule(); + repo = module.get(ClusterRepository); + competences = module.get(CompetenceRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a cluster row', async () => { + const created = await repo.create( + ClusterDomain.create({ + competenceId, + lowerBound: 0, + upperBound: 2, + title: 'Beginner', + description: 'desc', + }), + ); + + expect(created.id).toBeDefined(); + expect(created.competenceId).toBe(competenceId); + + const fromDb = await prisma.cluster.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Beginner'); + }); + }); + + describe('findById', () => { + it('returns the cluster when found', async () => { + const created = await repo.create( + ClusterDomain.create({ + competenceId, + lowerBound: 0, + upperBound: 1, + title: 'B', + description: '', + }), + ); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + ClusterDomain, + ); + }); + + it('returns null when missing', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await repo.create( + ClusterDomain.create({ + competenceId, + lowerBound: 0, + upperBound: 2, + title: 'Alpha', + description: 'a', + }), + ); + await repo.create( + ClusterDomain.create({ + competenceId, + lowerBound: 3, + upperBound: 5, + title: 'Beta', + description: 'b', + }), + ); + }); + + it('returns all when no filter is supplied', async () => { + const all = await repo.search({} as any); + expect(all).toHaveLength(2); + }); + + it('filters by competenceId', async () => { + const result = await repo.search({ competenceId } as any); + expect(result).toHaveLength(2); + }); + + it('honours descending sort on title', async () => { + const result = await repo.search({ + sortBy: ClusterSortField.TITLE, + sortDirection: SortDirection.DESC, + } as any); + const titles = result.map((c) => c.title); + expect(titles).toEqual([...titles].sort().reverse()); + }); + }); + + describe('updateById', () => { + it('persists the patched fields', async () => { + const created = await repo.create( + ClusterDomain.create({ + competenceId, + lowerBound: 0, + upperBound: 2, + title: 'B', + description: 'old', + }), + ); + + const updated = await repo.updateById(created.id!, { + description: 'new', + } as any); + + expect(updated.description).toBe('new'); + }); + }); + + describe('deleteById', () => { + it('removes the cluster row', async () => { + const created = await repo.create( + ClusterDomain.create({ + competenceId, + lowerBound: 0, + upperBound: 1, + title: 'B', + description: '', + }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.cluster.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/repository/competence-question-template-relation.repository.integration-spec.ts b/apps/api/test/integration/contexts/library/repository/competence-question-template-relation.repository.integration-spec.ts new file mode 100644 index 00000000..92021451 --- /dev/null +++ b/apps/api/test/integration/contexts/library/repository/competence-question-template-relation.repository.integration-spec.ts @@ -0,0 +1,162 @@ +import '../../../setup-env'; + +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CompetenceQuestionTemplateRelationDomain } from 'src/contexts/library/domain/competence-question-template-relation.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { CompetenceQuestionTemplateRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence-question-template-relation.repository'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { QuestionTemplateRepository } from 'src/contexts/library/infrastructure/prisma-repositories/question-template.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('CompetenceQuestionTemplateRelationRepository (integration)', () => { + let module: TestingModule; + let repo: CompetenceQuestionTemplateRelationRepository; + let competences: CompetenceRepository; + let templates: QuestionTemplateRepository; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createLibraryTestModule(); + repo = module.get(CompetenceQuestionTemplateRelationRepository); + competences = module.get(CompetenceRepository); + templates = module.get(QuestionTemplateRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + async function setup() { + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const template = await templates.create( + QuestionTemplateDomain.create({ + competenceId: competence.id!, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + status: QuestionTemplateStatus.ACTIVE, + }), + ); + return { + competenceId: competence.id!, + questionTemplateId: template.id!, + }; + } + + describe('link', () => { + it('creates a relation row', async () => { + const { competenceId, questionTemplateId } = await setup(); + + const created = await repo.link(competenceId, questionTemplateId); + + expect(created).toBeInstanceOf( + CompetenceQuestionTemplateRelationDomain, + ); + expect(created.competenceId).toBe(competenceId); + expect(created.questionTemplateId).toBe(questionTemplateId); + }); + + it('is idempotent: returns the existing row on duplicate link', async () => { + const { competenceId, questionTemplateId } = await setup(); + + const first = await repo.link(competenceId, questionTemplateId); + const second = await repo.link(competenceId, questionTemplateId); + + expect(second.id).toBe(first.id); + }); + }); + + describe('unlink', () => { + it('removes the relation when it exists', async () => { + const { competenceId, questionTemplateId } = await setup(); + await repo.link(competenceId, questionTemplateId); + + await repo.unlink(competenceId, questionTemplateId); + + const rows = + await prisma.competenceQuestionTemplateRelation.findMany({ + where: { competenceId, questionTemplateId }, + }); + expect(rows).toEqual([]); + }); + }); + + describe('listByCompetence / listByQuestionTemplate', () => { + it('lists relations attached to a competence', async () => { + const { competenceId, questionTemplateId: q1 } = await setup(); + // Second template under the same competence + const q2 = ( + await templates.create( + QuestionTemplateDomain.create({ + competenceId, + title: 'Q2', + answerType: AnswerType.TEXT_FIELD, + status: QuestionTemplateStatus.ACTIVE, + }), + ) + ).id!; + await repo.link(competenceId, q1); + await repo.link(competenceId, q2); + + const result = await repo.listByCompetence(competenceId); + + expect(result.map((r) => r.questionTemplateId).sort()).toEqual( + [q1, q2].sort(), + ); + }); + + it('lists relations attached to a question template', async () => { + const { competenceId: c1, questionTemplateId } = await setup(); + const c2 = ( + await competences.create( + CompetenceDomain.create({ title: 'Leadership' }), + ) + ).id!; + await repo.link(c1, questionTemplateId); + await repo.link(c2, questionTemplateId); + + const result = + await repo.listByQuestionTemplate(questionTemplateId); + + expect(result.map((r) => r.competenceId).sort()).toEqual( + [c1, c2].sort(), + ); + }); + }); + + describe('deleteAllForCompetence / deleteAllForQuestionTemplate', () => { + it('deleteAllForCompetence wipes the competence side', async () => { + const { competenceId, questionTemplateId } = await setup(); + await repo.link(competenceId, questionTemplateId); + + await repo.deleteAllForCompetence(competenceId); + + await expect(repo.listByCompetence(competenceId)).resolves.toEqual( + [], + ); + }); + + it('deleteAllForQuestionTemplate wipes the template side', async () => { + const { competenceId, questionTemplateId } = await setup(); + await repo.link(competenceId, questionTemplateId); + + await repo.deleteAllForQuestionTemplate(questionTemplateId); + + await expect( + repo.listByQuestionTemplate(questionTemplateId), + ).resolves.toEqual([]); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/repository/competence.repository.integration-spec.ts b/apps/api/test/integration/contexts/library/repository/competence.repository.integration-spec.ts new file mode 100644 index 00000000..9783db2c --- /dev/null +++ b/apps/api/test/integration/contexts/library/repository/competence.repository.integration-spec.ts @@ -0,0 +1,145 @@ +import '../../../setup-env'; + +import { CompetenceSortField, SortDirection } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('CompetenceRepository (integration)', () => { + let module: TestingModule; + let repo: CompetenceRepository; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createLibraryTestModule(); + repo = module.get(CompetenceRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a competence and round-trips through Prisma', async () => { + const created = await repo.create( + CompetenceDomain.create({ + code: 'C-1', + title: 'Teamwork', + description: 'Working in a team', + }), + ); + + expect(created.id).toBeDefined(); + expect(created.code).toBe('C-1'); + + const fromDb = await prisma.competence.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Teamwork'); + }); + }); + + describe('findById', () => { + it('returns the competence when found', async () => { + const created = await repo.create( + CompetenceDomain.create({ title: 'A' }), + ); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + CompetenceDomain, + ); + }); + + it('returns null when missing', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await repo.create( + CompetenceDomain.create({ + code: 'C-A', + title: 'Alpha', + description: 'first', + }), + ); + await repo.create( + CompetenceDomain.create({ + code: 'C-B', + title: 'Beta', + description: 'second', + }), + ); + await repo.create( + CompetenceDomain.create({ + code: 'C-C', + title: 'Gamma', + description: 'third', + }), + ); + }); + + it('returns all when no filter is supplied', async () => { + const all = await repo.search({} as any); + expect(all).toHaveLength(3); + }); + + it('filters by code (case insensitive substring)', async () => { + const result = await repo.search({ code: 'c-a' } as any); + expect(result.map((c) => c.code)).toEqual(['C-A']); + }); + + it('honours descending sort on title', async () => { + const result = await repo.search({ + sortBy: CompetenceSortField.TITLE, + sortDirection: SortDirection.DESC, + } as any); + + const titles = result.map((c) => c.title); + expect(titles).toEqual([...titles].sort().reverse()); + }); + }); + + describe('updateById', () => { + it('persists the patched fields', async () => { + const created = await repo.create( + CompetenceDomain.create({ + title: 'Old', + description: 'old', + }), + ); + + const updated = await repo.updateById(created.id!, { + description: 'new', + } as any); + + expect(updated.description).toBe('new'); + }); + }); + + describe('deleteById', () => { + it('removes the competence row', async () => { + const created = await repo.create( + CompetenceDomain.create({ title: 'Doomed' }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.competence.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/repository/position-competence-relation.repository.integration-spec.ts b/apps/api/test/integration/contexts/library/repository/position-competence-relation.repository.integration-spec.ts new file mode 100644 index 00000000..09cab771 --- /dev/null +++ b/apps/api/test/integration/contexts/library/repository/position-competence-relation.repository.integration-spec.ts @@ -0,0 +1,186 @@ +import '../../../setup-env'; + +import { TestingModule } from '@nestjs/testing'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { PositionCompetenceRelationDomain } from 'src/contexts/library/domain/position-competence-relation.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionCompetenceRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-competence-relation.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('PositionCompetenceRelationRepository (integration)', () => { + let module: TestingModule; + let repo: PositionCompetenceRelationRepository; + let competences: CompetenceRepository; + let positions: PositionRepository; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createLibraryTestModule(); + repo = module.get(PositionCompetenceRelationRepository); + competences = module.get(CompetenceRepository); + positions = module.get(PositionRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + async function setup() { + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + return { competenceId: competence.id!, positionId: position.id! }; + } + + describe('link', () => { + it('creates a relation row', async () => { + const { competenceId, positionId } = await setup(); + + const created = await repo.link(competenceId, positionId); + + expect(created).toBeInstanceOf(PositionCompetenceRelationDomain); + expect(created.competenceId).toBe(competenceId); + expect(created.positionId).toBe(positionId); + }); + + it('is idempotent: returns the existing row on duplicate link', async () => { + const { competenceId, positionId } = await setup(); + + const first = await repo.link(competenceId, positionId); + const second = await repo.link(competenceId, positionId); + + expect(second.id).toBe(first.id); + + const rows = await prisma.positionCompetenceRelation.findMany({ + where: { competenceId, positionId }, + }); + expect(rows).toHaveLength(1); + }); + }); + + describe('unlink', () => { + it('removes the relation when it exists', async () => { + const { competenceId, positionId } = await setup(); + await repo.link(competenceId, positionId); + + await repo.unlink(competenceId, positionId); + + const rows = await prisma.positionCompetenceRelation.findMany({ + where: { competenceId, positionId }, + }); + expect(rows).toEqual([]); + }); + + it('does not throw when the relation is absent', async () => { + const { competenceId, positionId } = await setup(); + await expect( + repo.unlink(competenceId, positionId), + ).resolves.toBeUndefined(); + }); + }); + + describe('listByCompetence / listByPosition', () => { + it('lists all relations for a competence', async () => { + const { competenceId, positionId: p1 } = await setup(); + const p2 = ( + await positions.create( + PositionDomain.create({ title: 'Manager' }), + ) + ).id!; + await repo.link(competenceId, p1); + await repo.link(competenceId, p2); + + const result = await repo.listByCompetence(competenceId); + + expect(result.map((r) => r.positionId).sort()).toEqual( + [p1, p2].sort(), + ); + }); + + it('lists all relations for a position', async () => { + const { competenceId: c1, positionId } = await setup(); + const c2 = ( + await competences.create( + CompetenceDomain.create({ title: 'Leadership' }), + ) + ).id!; + await repo.link(c1, positionId); + await repo.link(c2, positionId); + + const result = await repo.listByPosition(positionId); + + expect(result.map((r) => r.competenceId).sort()).toEqual( + [c1, c2].sort(), + ); + }); + }); + + describe('replaceForCompetence', () => { + it('keeps the supplied positions and drops the others', async () => { + const { competenceId, positionId: p1 } = await setup(); + const p2 = ( + await positions.create( + PositionDomain.create({ title: 'Manager' }), + ) + ).id!; + const p3 = ( + await positions.create( + PositionDomain.create({ title: 'Director' }), + ) + ).id!; + await repo.link(competenceId, p1); + await repo.link(competenceId, p2); + + const result = await repo.replaceForCompetence(competenceId, [ + p2, + p3, + ]); + + expect(result.map((r) => r.positionId).sort()).toEqual( + [p2, p3].sort(), + ); + }); + + it('removes all relations when the new list is empty', async () => { + const { competenceId, positionId } = await setup(); + await repo.link(competenceId, positionId); + + const result = await repo.replaceForCompetence(competenceId, []); + + expect(result).toEqual([]); + }); + }); + + describe('deleteAllForCompetence', () => { + it('removes every relation tied to the competence', async () => { + const { competenceId, positionId: p1 } = await setup(); + const p2 = ( + await positions.create( + PositionDomain.create({ title: 'Manager' }), + ) + ).id!; + await repo.link(competenceId, p1); + await repo.link(competenceId, p2); + + await repo.deleteAllForCompetence(competenceId); + + await expect(repo.listByCompetence(competenceId)).resolves.toEqual( + [], + ); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/repository/position-question-template-relation.repository.integration-spec.ts b/apps/api/test/integration/contexts/library/repository/position-question-template-relation.repository.integration-spec.ts new file mode 100644 index 00000000..5698dc0f --- /dev/null +++ b/apps/api/test/integration/contexts/library/repository/position-question-template-relation.repository.integration-spec.ts @@ -0,0 +1,194 @@ +import '../../../setup-env'; + +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { PositionQuestionTemplateRelationDomain } from 'src/contexts/library/domain/position-question-template-relation.domain'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionQuestionTemplateRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-question-template-relation.repository'; +import { QuestionTemplateRepository } from 'src/contexts/library/infrastructure/prisma-repositories/question-template.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('PositionQuestionTemplateRelationRepository (integration)', () => { + let module: TestingModule; + let repo: PositionQuestionTemplateRelationRepository; + let templates: QuestionTemplateRepository; + let competences: CompetenceRepository; + let positions: PositionRepository; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createLibraryTestModule(); + repo = module.get(PositionQuestionTemplateRelationRepository); + templates = module.get(QuestionTemplateRepository); + competences = module.get(CompetenceRepository); + positions = module.get(PositionRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + async function setup() { + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const template = await templates.create( + QuestionTemplateDomain.create({ + competenceId: competence.id!, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + status: QuestionTemplateStatus.ACTIVE, + }), + ); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + return { templateId: template.id!, positionId: position.id! }; + } + + describe('link', () => { + it('creates a relation row', async () => { + const { templateId, positionId } = await setup(); + + const created = await repo.link(templateId, positionId); + + expect(created).toBeInstanceOf( + PositionQuestionTemplateRelationDomain, + ); + expect(created.questionTemplateId).toBe(templateId); + expect(created.positionId).toBe(positionId); + }); + + it('is idempotent: returns the existing row on duplicate link', async () => { + const { templateId, positionId } = await setup(); + + const first = await repo.link(templateId, positionId); + const second = await repo.link(templateId, positionId); + + expect(second.id).toBe(first.id); + + const rows = await prisma.positionQuestionTemplateRelation.findMany( + { + where: { + questionTemplateId: templateId, + positionId, + }, + }, + ); + expect(rows).toHaveLength(1); + }); + }); + + describe('unlink', () => { + it('removes the relation when it exists', async () => { + const { templateId, positionId } = await setup(); + await repo.link(templateId, positionId); + + await repo.unlink(templateId, positionId); + + const rows = await prisma.positionQuestionTemplateRelation.findMany( + { + where: { + questionTemplateId: templateId, + positionId, + }, + }, + ); + expect(rows).toEqual([]); + }); + }); + + describe('listByQuestion', () => { + it('returns all relations attached to a question template', async () => { + const { templateId, positionId: p1 } = await setup(); + const p2 = ( + await positions.create( + PositionDomain.create({ title: 'Manager' }), + ) + ).id!; + await repo.link(templateId, p1); + await repo.link(templateId, p2); + + const result = await repo.listByQuestion(templateId); + + expect(result.map((r) => r.positionId).sort()).toEqual( + [p1, p2].sort(), + ); + }); + }); + + describe('replace', () => { + it('keeps the supplied positions and drops the others', async () => { + const { templateId, positionId: p1 } = await setup(); + const p2 = ( + await positions.create( + PositionDomain.create({ title: 'Manager' }), + ) + ).id!; + const p3 = ( + await positions.create( + PositionDomain.create({ title: 'Director' }), + ) + ).id!; + await repo.link(templateId, p1); + await repo.link(templateId, p2); + + const result = await repo.replace(templateId, [p2, p3]); + + expect(result.map((r) => r.positionId).sort()).toEqual( + [p2, p3].sort(), + ); + }); + + it('removes all relations when the new list is empty', async () => { + const { templateId, positionId } = await setup(); + await repo.link(templateId, positionId); + + const result = await repo.replace(templateId, []); + + expect(result).toEqual([]); + }); + + it('deduplicates the input list', async () => { + const { templateId, positionId } = await setup(); + + const result = await repo.replace(templateId, [ + positionId, + positionId, + ]); + + expect(result).toHaveLength(1); + expect(result[0].positionId).toBe(positionId); + }); + }); + + describe('deleteAllForQuestionTemplate', () => { + it('removes every relation tied to the template', async () => { + const { templateId, positionId: p1 } = await setup(); + const p2 = ( + await positions.create( + PositionDomain.create({ title: 'Manager' }), + ) + ).id!; + await repo.link(templateId, p1); + await repo.link(templateId, p2); + + await repo.deleteAllForQuestionTemplate(templateId); + + await expect(repo.listByQuestion(templateId)).resolves.toEqual([]); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/repository/question-template.repository.integration-spec.ts b/apps/api/test/integration/contexts/library/repository/question-template.repository.integration-spec.ts new file mode 100644 index 00000000..91e0a92a --- /dev/null +++ b/apps/api/test/integration/contexts/library/repository/question-template.repository.integration-spec.ts @@ -0,0 +1,198 @@ +import '../../../setup-env'; + +import { + AnswerType, + QuestionTemplateSortField, + QuestionTemplateStatus, + SortDirection, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { QuestionTemplateRepository } from 'src/contexts/library/infrastructure/prisma-repositories/question-template.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('QuestionTemplateRepository (integration)', () => { + let module: TestingModule; + let repo: QuestionTemplateRepository; + let competences: CompetenceRepository; + let positions: PositionRepository; + let prisma: PrismaService; + let competenceId: number; + + beforeAll(async () => { + module = await createLibraryTestModule(); + repo = module.get(QuestionTemplateRepository); + competences = module.get(CompetenceRepository); + positions = module.get(PositionRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a template with no position relations', async () => { + const created = await repo.create( + QuestionTemplateDomain.create({ + competenceId, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + status: QuestionTemplateStatus.ACTIVE, + }), + ); + + expect(created.id).toBeDefined(); + expect(created.positionIds).toEqual([]); + }); + + it('persists the supplied position relations', async () => { + const p1 = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const p2 = await positions.create( + PositionDomain.create({ title: 'Manager' }), + ); + + const created = await repo.create( + QuestionTemplateDomain.create({ + competenceId, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + positionIds: [p1.id!, p2.id!], + }), + ); + + expect(created.positionIds.sort()).toEqual([p1.id!, p2.id!].sort()); + + const rows = await prisma.positionQuestionTemplateRelation.findMany( + { + where: { questionTemplateId: created.id! }, + }, + ); + expect(rows).toHaveLength(2); + }); + }); + + describe('findById', () => { + it('returns the template when found', async () => { + const created = await repo.create( + QuestionTemplateDomain.create({ + competenceId, + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + }), + ); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + QuestionTemplateDomain, + ); + }); + + it('returns null when missing', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await repo.create( + QuestionTemplateDomain.create({ + competenceId, + title: 'Alpha', + answerType: AnswerType.TEXT_FIELD, + status: QuestionTemplateStatus.ACTIVE, + }), + ); + await repo.create( + QuestionTemplateDomain.create({ + competenceId, + title: 'Beta', + answerType: AnswerType.NUMERICAL_SCALE, + status: QuestionTemplateStatus.ARCHIVE, + }), + ); + }); + + it('returns all when no filter is supplied', async () => { + const all = await repo.search({} as any); + expect(all).toHaveLength(2); + }); + + it('filters by status', async () => { + const result = await repo.search({ + status: QuestionTemplateStatus.ARCHIVE, + } as any); + expect(result.map((q) => q.title)).toEqual(['Beta']); + }); + + it('filters by answerType', async () => { + const result = await repo.search({ + answerType: AnswerType.NUMERICAL_SCALE, + } as any); + expect(result.map((q) => q.title)).toEqual(['Beta']); + }); + + it('honours descending sort on title', async () => { + const result = await repo.search({ + sortBy: QuestionTemplateSortField.TITLE, + sortDirection: SortDirection.DESC, + } as any); + const titles = result.map((q) => q.title); + expect(titles).toEqual([...titles].sort().reverse()); + }); + }); + + describe('updateById', () => { + it('persists the patched scalar fields', async () => { + const created = await repo.create( + QuestionTemplateDomain.create({ + competenceId, + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + }), + ); + + const updated = await repo.updateById(created.id!, { + title: 'NEW', + } as any); + + expect(updated.title).toBe('NEW'); + }); + }); + + describe('deleteById', () => { + it('removes the template row', async () => { + const created = await repo.create( + QuestionTemplateDomain.create({ + competenceId, + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.questionTemplate.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/service/cluster.service.integration-spec.ts b/apps/api/test/integration/contexts/library/service/cluster.service.integration-spec.ts new file mode 100644 index 00000000..f81e74bf --- /dev/null +++ b/apps/api/test/integration/contexts/library/service/cluster.service.integration-spec.ts @@ -0,0 +1,217 @@ +import '../../../setup-env'; + +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { ClusterService } from 'src/contexts/library/application/services/cluster.service'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('ClusterService (integration)', () => { + let module: TestingModule; + let service: ClusterService; + let competences: CompetenceService; + let prisma: PrismaService; + let competenceId: number; + + beforeAll(async () => { + module = await createLibraryTestModule(); + service = module.get(ClusterService); + competences = module.get(CompetenceService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + const competence = await competences.create({ + title: 'Teamwork', + } as any); + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a cluster bound to the competence', async () => { + const created = await service.create({ + competenceId, + lowerBound: 0, + upperBound: 2, + title: 'Beginner', + description: 'Just starting out', + }); + + expect(created.id).toBeDefined(); + expect(created.competenceId).toBe(competenceId); + expect(created.lowerBound).toBe(0); + expect(created.upperBound).toBe(2); + + const fromDb = await prisma.cluster.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Beginner'); + }); + + it('rejects creation when lowerBound > upperBound', async () => { + await expect( + service.create({ + competenceId, + lowerBound: 5, + upperBound: 2, + title: 'Invalid', + description: '', + }), + ).rejects.toBeInstanceOf(BadRequestException); + + const rows = await prisma.cluster.findMany(); + expect(rows).toHaveLength(0); + }); + + it('throws NotFoundException when the competence is missing', async () => { + await expect( + service.create({ + competenceId: 999999, + lowerBound: 0, + upperBound: 1, + title: 'Orphan', + description: '', + }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await service.create({ + competenceId, + lowerBound: 0, + upperBound: 2, + title: 'Beginner', + description: 'a', + }); + await service.create({ + competenceId, + lowerBound: 3, + upperBound: 5, + title: 'Intermediate', + description: 'b', + }); + }); + + it('returns all clusters when no filter is supplied', async () => { + const all = await service.search({} as any); + expect(all).toHaveLength(2); + }); + + it('filters by title (case insensitive substring)', async () => { + const result = await service.search({ + title: 'beginner', + } as any); + expect(result.map((c) => c.title)).toEqual(['Beginner']); + }); + }); + + describe('getById', () => { + it('returns the cluster when found', async () => { + const created = await service.create({ + competenceId, + lowerBound: 0, + upperBound: 1, + title: 'B', + description: '', + }); + + await expect(service.getById(created.id!)).resolves.toBeInstanceOf( + ClusterDomain, + ); + }); + + it('throws NotFoundException for a missing id', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('persists patched fields', async () => { + const created = await service.create({ + competenceId, + lowerBound: 0, + upperBound: 2, + title: 'B', + description: 'old', + }); + + const updated = await service.update(created.id!, { + description: 'new', + } as any); + + expect(updated.description).toBe('new'); + }); + + it('rejects when patched bounds are inverted', async () => { + const created = await service.create({ + competenceId, + lowerBound: 0, + upperBound: 2, + title: 'B', + description: '', + }); + + await expect( + service.update(created.id!, { + lowerBound: 10, + upperBound: 1, + } as any), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('validates the new competenceId exists when changed', async () => { + const created = await service.create({ + competenceId, + lowerBound: 0, + upperBound: 2, + title: 'B', + description: '', + }); + + await expect( + service.update(created.id!, { + competenceId: 999999, + } as any), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('delete', () => { + it('removes the cluster row', async () => { + const created = await service.create({ + competenceId, + lowerBound: 0, + upperBound: 1, + title: 'B', + description: '', + }); + + await service.delete(created.id!); + + const fromDb = await prisma.cluster.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('throws NotFoundException for a missing cluster', async () => { + await expect(service.delete(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/service/competence.service.integration-spec.ts b/apps/api/test/integration/contexts/library/service/competence.service.integration-spec.ts new file mode 100644 index 00000000..7a0198c1 --- /dev/null +++ b/apps/api/test/integration/contexts/library/service/competence.service.integration-spec.ts @@ -0,0 +1,327 @@ +import '../../../setup-env'; + +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('CompetenceService (integration)', () => { + let module: TestingModule; + let service: CompetenceService; + let positions: PositionService; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createLibraryTestModule(); + service = module.get(CompetenceService); + positions = module.get(PositionService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a new competence with the supplied fields', async () => { + const created = await service.create({ + code: 'C-1', + title: 'Teamwork', + description: 'Working in a team', + }); + + expect(created.id).toBeDefined(); + expect(created.code).toBe('C-1'); + expect(created.title).toBe('Teamwork'); + expect(created.description).toBe('Working in a team'); + + const fromDb = await prisma.competence.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Teamwork'); + expect(fromDb!.code).toBe('C-1'); + }); + + it('coerces missing code/description to null', async () => { + const created = await service.create({ + title: 'Leadership', + } as any); + + expect(created.code).toBeNull(); + expect(created.description).toBeNull(); + + const fromDb = await prisma.competence.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.code).toBeNull(); + expect(fromDb!.description).toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await service.create({ + code: 'C-A', + title: 'Alpha', + description: 'first', + }); + await service.create({ + code: 'C-B', + title: 'Beta', + description: 'second', + }); + await service.create({ + code: 'C-C', + title: 'Gamma communication', + description: 'third', + }); + }); + + it('returns all competences when no filter is supplied', async () => { + const all = await service.search({} as any); + expect(all).toHaveLength(3); + }); + + it('filters by title substring (case insensitive)', async () => { + const result = await service.search({ title: 'GAMMA' } as any); + expect(result.map((c) => c.title)).toEqual(['Gamma communication']); + }); + + it('filters by free-text "search" across title/code/description', async () => { + const result = await service.search({ search: 'second' } as any); + expect(result.map((c) => c.title)).toEqual(['Beta']); + }); + }); + + describe('getById', () => { + it('returns the competence when found', async () => { + const created = await service.create({ + title: 'Communication', + } as any); + + await expect(service.getById(created.id!)).resolves.toBeInstanceOf( + CompetenceDomain, + ); + }); + + it('throws NotFoundException for a missing id', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('persists the patched fields and ignores undefined keys', async () => { + const created = await service.create({ + title: 'Old', + description: 'old desc', + } as any); + + const updated = await service.update(created.id!, { + title: 'New', + } as any); + + expect(updated.title).toBe('New'); + expect(updated.description).toBe('old desc'); + + const fromDb = await prisma.competence.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('New'); + expect(fromDb!.description).toBe('old desc'); + }); + + it('throws NotFoundException when the competence is missing', async () => { + await expect( + service.update(999999, { title: 'X' } as any), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('delete', () => { + it('removes the competence when nothing is linked', async () => { + const created = await service.create({ title: 'Doomed' } as any); + + await service.delete(created.id!); + + const fromDb = await prisma.competence.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('throws NotFoundException for a missing competence', async () => { + await expect(service.delete(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('rejects deletion when at least one position is linked', async () => { + const competence = await service.create({ + title: 'Linked', + } as any); + const position = await positions.create({ title: 'Engineer' }); + + await prisma.positionCompetenceRelation.create({ + data: { + positionId: position.id!, + competenceId: competence.id!, + }, + }); + + await expect(service.delete(competence.id!)).rejects.toBeInstanceOf( + BadRequestException, + ); + + const stillThere = await prisma.competence.findUnique({ + where: { id: competence.id! }, + }); + expect(stillThere).not.toBeNull(); + }); + + it('rejects deletion when at least one question template is linked', async () => { + const competence = await service.create({ + title: 'Linked', + } as any); + const template = await prisma.questionTemplate.create({ + data: { + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: competence.id!, + status: QuestionTemplateStatus.ACTIVE, + }, + }); + await prisma.competenceQuestionTemplateRelation.create({ + data: { + competenceId: competence.id!, + questionTemplateId: template.id, + }, + }); + + await expect(service.delete(competence.id!)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + }); + + describe('attachPosition / detachPosition / listPositions', () => { + it('links a competence to a position and returns the new list', async () => { + const competence = await service.create({ + title: 'Communication', + } as any); + const position = await positions.create({ title: 'Engineer' }); + + const ids = await service.attachPosition( + competence.id!, + position.id!, + ); + + expect(ids).toEqual([position.id]); + }); + + it('is idempotent: attaching the same position twice keeps a single row', async () => { + const competence = await service.create({ title: 'C' } as any); + const position = await positions.create({ title: 'P' }); + + await service.attachPosition(competence.id!, position.id!); + const ids = await service.attachPosition( + competence.id!, + position.id!, + ); + + expect(ids).toEqual([position.id]); + + const rows = await prisma.positionCompetenceRelation.findMany({ + where: { competenceId: competence.id! }, + }); + expect(rows).toHaveLength(1); + }); + + it('throws NotFoundException when the position is missing', async () => { + const competence = await service.create({ title: 'C' } as any); + + await expect( + service.attachPosition(competence.id!, 999999), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('detachPosition removes the relation', async () => { + const competence = await service.create({ title: 'C' } as any); + const position = await positions.create({ title: 'P' }); + await service.attachPosition(competence.id!, position.id!); + + await service.detachPosition(competence.id!, position.id!); + + await expect( + service.listPositions(competence.id!), + ).resolves.toEqual([]); + }); + + it('detachPosition is a no-op when no relation exists', async () => { + const competence = await service.create({ title: 'C' } as any); + const position = await positions.create({ title: 'P' }); + + await expect( + service.detachPosition(competence.id!, position.id!), + ).resolves.toBeUndefined(); + }); + }); + + describe('attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates', () => { + async function makeTemplate(competenceId: number): Promise { + const created = await prisma.questionTemplate.create({ + data: { + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId, + status: QuestionTemplateStatus.ACTIVE, + }, + }); + return created.id; + } + + it('links and returns the templates attached to the competence', async () => { + const competence = await service.create({ title: 'C' } as any); + const templateId = await makeTemplate(competence.id!); + + const ids = await service.attachQuestionTemplate( + competence.id!, + templateId, + ); + + expect(ids).toEqual([templateId]); + }); + + it('throws NotFoundException when the question template is missing', async () => { + const competence = await service.create({ title: 'C' } as any); + + await expect( + service.attachQuestionTemplate(competence.id!, 999999), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('detachQuestionTemplate removes the relation', async () => { + const competence = await service.create({ title: 'C' } as any); + const templateId = await makeTemplate(competence.id!); + await service.attachQuestionTemplate(competence.id!, templateId); + + await service.detachQuestionTemplate(competence.id!, templateId); + + await expect( + service.listQuestionTemplates(competence.id!), + ).resolves.toEqual([]); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/service/question-template.service.integration-spec.ts b/apps/api/test/integration/contexts/library/service/question-template.service.integration-spec.ts new file mode 100644 index 00000000..374f11b6 --- /dev/null +++ b/apps/api/test/integration/contexts/library/service/question-template.service.integration-spec.ts @@ -0,0 +1,311 @@ +import '../../../setup-env'; + +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createLibraryTestModule, + resetLibraryTables, +} from '../test-app-library'; + +describe('QuestionTemplateService (integration)', () => { + let module: TestingModule; + let service: QuestionTemplateService; + let competences: CompetenceService; + let positions: PositionService; + let prisma: PrismaService; + let competenceId: number; + + beforeAll(async () => { + module = await createLibraryTestModule(); + service = module.get(QuestionTemplateService); + competences = module.get(CompetenceService); + positions = module.get(PositionService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetLibraryTables(prisma); + const competence = await competences.create({ + title: 'Teamwork', + } as any); + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a template with defaults applied', async () => { + const created = await service.create({ + competenceId, + title: 'Rate teamwork', + answerType: AnswerType.NUMERICAL_SCALE, + } as any); + + expect(created).toBeInstanceOf(QuestionTemplateDomain); + expect(created.competenceId).toBe(competenceId); + expect(created.status).toBe(QuestionTemplateStatus.ACTIVE); + expect(created.isForSelfassessment).toBe(false); + expect(created.positionIds).toEqual([]); + + const fromDb = await prisma.questionTemplate.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Rate teamwork'); + }); + + it('throws NotFoundException when the competence is missing', async () => { + await expect( + service.create({ + competenceId: 999999, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('attaches the supplied positions', async () => { + const p1 = await positions.create({ title: 'Engineer' }); + const p2 = await positions.create({ title: 'Manager' }); + + const created = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + positionIds: [p1.id!, p2.id!], + } as any); + + expect(created.positionIds.sort()).toEqual([p1.id!, p2.id!].sort()); + + const rows = await prisma.positionQuestionTemplateRelation.findMany( + { + where: { questionTemplateId: created.id! }, + }, + ); + expect(rows).toHaveLength(2); + }); + + it('rejects creation when one of the supplied positions does not exist', async () => { + const p1 = await positions.create({ title: 'Engineer' }); + + await expect( + service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + positionIds: [p1.id!, 999999], + } as any), + ).rejects.toThrow(); + }); + }); + + describe('search / getById', () => { + it('search filters by competenceId', async () => { + const other = await competences.create({ + title: 'Other', + } as any); + + await service.create({ + competenceId, + title: 'A', + answerType: AnswerType.TEXT_FIELD, + } as any); + await service.create({ + competenceId: other.id!, + title: 'B', + answerType: AnswerType.TEXT_FIELD, + } as any); + + const result = await service.search({ competenceId } as any); + + expect(result).toHaveLength(1); + expect(result[0].title).toBe('A'); + }); + + it('getById throws NotFoundException when missing', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('persists patched scalar fields', async () => { + const created = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + + const updated = await service.update(created.id!, { + title: 'Q2', + } as any); + + expect(updated.title).toBe('Q2'); + + const fromDb = await prisma.questionTemplate.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Q2'); + }); + + it('validates the new competenceId when changed', async () => { + const created = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + + await expect( + service.update(created.id!, { + competenceId: 999999, + } as any), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('delete', () => { + it('removes the template and its relations when nothing references it', async () => { + const created = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + + await service.delete(created.id!); + + const fromDb = await prisma.questionTemplate.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('rejects deletion when a review question references the template', async () => { + const template = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + } as any); + + await prisma.question.create({ + data: { + title: 'Q?', + answerType: 'NUMERICAL_SCALE' as any, + questionTemplateId: template.id!, + }, + }); + + await expect(service.delete(template.id!)).rejects.toBeInstanceOf( + BadRequestException, + ); + + const stillThere = await prisma.questionTemplate.findUnique({ + where: { id: template.id! }, + }); + expect(stillThere).not.toBeNull(); + }); + }); + + describe('attachPosition / detachPosition / listPositions', () => { + it('links a template to a position and lists it back', async () => { + const template = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + const position = await positions.create({ title: 'Engineer' }); + + const ids = await service.attachPosition( + template.id!, + position.id!, + ); + + expect(ids).toEqual([position.id]); + }); + + it('throws NotFoundException when the position does not exist', async () => { + const template = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + + await expect( + service.attachPosition(template.id!, 999999), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('detachPosition removes the relation', async () => { + const template = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + const position = await positions.create({ title: 'Engineer' }); + await service.attachPosition(template.id!, position.id!); + + await service.detachPosition(template.id!, position.id!); + + await expect(service.listPositions(template.id!)).resolves.toEqual( + [], + ); + }); + }); + + describe('attachCompetence / detachCompetence / listCompetences', () => { + it('links a template to a competence and lists it back', async () => { + const template = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + const otherCompetence = await competences.create({ + title: 'Other', + } as any); + + const ids = await service.attachCompetence( + template.id!, + otherCompetence.id!, + ); + + expect(ids).toEqual([otherCompetence.id]); + }); + + it('throws NotFoundException when the competence does not exist', async () => { + const template = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + + await expect( + service.attachCompetence(template.id!, 999999), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('detachCompetence removes the relation', async () => { + const template = await service.create({ + competenceId, + title: 'Q', + answerType: AnswerType.TEXT_FIELD, + } as any); + const otherCompetence = await competences.create({ + title: 'Other', + } as any); + await service.attachCompetence(template.id!, otherCompetence.id!); + + await service.detachCompetence(template.id!, otherCompetence.id!); + + await expect( + service.listCompetences(template.id!), + ).resolves.toEqual([]); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/library/test-app-library.ts b/apps/api/test/integration/contexts/library/test-app-library.ts new file mode 100644 index 00000000..95646b66 --- /dev/null +++ b/apps/api/test/integration/contexts/library/test-app-library.ts @@ -0,0 +1,143 @@ +import { ConfigModule } from '@nestjs/config'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { Test, TestingModule } from '@nestjs/testing'; +import appConfig from 'src/config/app'; +import databaseConfig from 'src/config/database'; +import { QUESTION_REPOSITORY } from 'src/contexts/feedback360/application/ports/question.repository.port'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { IDENTITY_ROLE_REPOSITORY } from 'src/contexts/identity/application/ports/role.repository.port'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { RoleRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/role.repository'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { CLUSTER_REPOSITORY } from 'src/contexts/library/application/ports/cluster.repository.port'; +import { COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/competence-question-template-relation.repository.port'; +import { COMPETENCE_REPOSITORY } from 'src/contexts/library/application/ports/competence.repository.port'; +import { POSITION_COMPETENCE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-competence-relation.repository.port'; +import { POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-question-template-relation.repository.port'; +import { QUESTION_TEMPLATE_REPOSITORY } from 'src/contexts/library/application/ports/question-template.repository.port'; +import { ClusterService } from 'src/contexts/library/application/services/cluster.service'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service'; +import { ClusterRepository } from 'src/contexts/library/infrastructure/prisma-repositories/cluster.repository'; +import { CompetenceQuestionTemplateRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence-question-template-relation.repository'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionCompetenceRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-competence-relation.repository'; +import { PositionQuestionTemplateRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-question-template-relation.repository'; +import { QuestionTemplateRepository } from 'src/contexts/library/infrastructure/prisma-repositories/question-template.repository'; +import { ORGANISATION_POSITION_REPOSITORY } from 'src/contexts/organisation/application/ports/position.repository.port'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { DatabaseModule } from 'src/database/database.module'; +import { PrismaService } from 'src/database/prisma.service'; +import { ensureTestDatabaseExists } from '../../setup-env'; + +/** + * Bootstraps a Nest testing module for the library context wired to + * the real database. We assemble the providers manually instead of + * importing `LibraryModule` because that module pulls in `AuthModule` + * and the full `Feedback360Module`, which would require the entire + * HTTP / better-auth setup for service-level tests that never need it. + * + * Cross-context dependencies wired in: + * - Organisation `PositionService` + `PositionRepository` + * (`CompetenceService.attachPosition` and `QuestionTemplateService` + * validate positions against real rows in `org_positions`). + * - Identity providers (for completeness in case future tests use + * them). + * - Feedback360 `QuestionRepository` bound under `QUESTION_REPOSITORY` + * so `QuestionTemplateService.delete()` can check for related review + * questions in `feedback360_questions`. + */ +export async function createLibraryTestModule(): Promise { + await ensureTestDatabaseExists(); + + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig], + ignoreEnvFile: true, + }), + EventEmitterModule.forRoot(), + DatabaseModule, + ], + providers: [ + // Library + CompetenceService, + ClusterService, + QuestionTemplateService, + CompetenceRepository, + ClusterRepository, + QuestionTemplateRepository, + PositionCompetenceRelationRepository, + PositionQuestionTemplateRelationRepository, + CompetenceQuestionTemplateRelationRepository, + { + provide: COMPETENCE_REPOSITORY, + useExisting: CompetenceRepository, + }, + { provide: CLUSTER_REPOSITORY, useExisting: ClusterRepository }, + { + provide: QUESTION_TEMPLATE_REPOSITORY, + useExisting: QuestionTemplateRepository, + }, + { + provide: POSITION_COMPETENCE_RELATION_REPOSITORY, + useExisting: PositionCompetenceRelationRepository, + }, + { + provide: POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useExisting: PositionQuestionTemplateRelationRepository, + }, + { + provide: COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useExisting: CompetenceQuestionTemplateRelationRepository, + }, + // Organisation (PositionService is required by library services) + PositionService, + PositionRepository, + { + provide: ORGANISATION_POSITION_REPOSITORY, + useExisting: PositionRepository, + }, + // Feedback360 question repo — used by QuestionTemplateService.delete + QuestionRepository, + { provide: QUESTION_REPOSITORY, useExisting: QuestionRepository }, + // Identity (kept for completeness; some library scenarios may + // need ratees / managers in future) + IdentityUserService, + IdentityRoleService, + UserRepository, + RoleRepository, + { provide: IDENTITY_USER_REPOSITORY, useExisting: UserRepository }, + { provide: IDENTITY_ROLE_REPOSITORY, useExisting: RoleRepository }, + ], + }).compile(); + + await module.init(); + return module; +} + +/** + * Truncates the library-owned tables and their cross-context + * dependencies between tests. `identity_roles` reference data is + * preserved; everything else relevant to library scenarios is wiped. + */ +export async function resetLibraryTables(prisma: PrismaService): Promise { + await prisma.$executeRawUnsafe( + `TRUNCATE TABLE + "feedback360_questions", + "library_competence_question_template_relations", + "library_position_question_template_relations", + "library_position_competence_relations", + "library_clusters", + "library_question_templates", + "library_competences", + "org_positions", + "identity_users_roles", + "identity_users" + RESTART IDENTITY CASCADE`, + ); +} diff --git a/apps/api/test/integration/contexts/notifications/listeners/notification-listeners.integration-spec.ts b/apps/api/test/integration/contexts/notifications/listeners/notification-listeners.integration-spec.ts new file mode 100644 index 00000000..fa200d3a --- /dev/null +++ b/apps/api/test/integration/contexts/notifications/listeners/notification-listeners.integration-spec.ts @@ -0,0 +1,284 @@ +import '../../../setup-env'; + +import { CycleStage, ReviewStage } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleStageChangedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-changed.event'; +import { ReviewStageChangedEvent } from 'src/contexts/feedback360/application/events/review-stage-changed.event'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { UserCreatedEvent } from 'src/contexts/identity/application/events/user-created.event'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { CycleStageNotificationListener } from 'src/contexts/notifications/application/listeners/cycle-stage-notification.listener'; +import { ReviewStageNotificationListener } from 'src/contexts/notifications/application/listeners/review-stage-notification.listener'; +import { UserCreatedNotificationListener } from 'src/contexts/notifications/application/listeners/user-created-notification.listener'; +import { NotificationKind } from 'src/contexts/notifications/domain/notification-kind.enum'; +import { NotificationLogRepository } from 'src/contexts/notifications/infrastructure/prisma-repositories/notification-log.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + MailerStub, + createNotificationsServiceTestModule, + resetNotificationTables, +} from '../test-app-notifications'; + +describe('Notification listeners (integration)', () => { + let module: TestingModule; + let mailer: MailerStub; + let cycleStageListener: CycleStageNotificationListener; + let reviewStageListener: ReviewStageNotificationListener; + let userCreatedListener: UserCreatedNotificationListener; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let positionRepo: PositionRepository; + let notificationLogRepo: NotificationLogRepository; + let userRepo: UserRepository; + let prisma: PrismaService; + + let hrId: number; + let rateeId: number; + let positionId: number; + + beforeAll(async () => { + const built = await createNotificationsServiceTestModule(); + module = built.module; + mailer = built.mailerStub; + cycleStageListener = module.get(CycleStageNotificationListener); + reviewStageListener = module.get(ReviewStageNotificationListener); + userCreatedListener = module.get(UserCreatedNotificationListener); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + positionRepo = module.get(PositionRepository); + notificationLogRepo = module.get(NotificationLogRepository); + userRepo = module.get(UserRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetNotificationTables(prisma); + mailer.reset(); + // Seed users via the repo directly (NOT via IdentityUserService) + // because the latter emits `user.created`, which would fire the + // welcome listener under test and pollute mailer.calls in every + // unrelated case. + const hr = await userRepo.create( + UserDomain.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + }), + ); + const ratee = await userRepo.create( + UserDomain.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + }), + ); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function makeCycle(): Promise { + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Q1 2026', + hrId, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + return cycle.id!; + } + + async function makeReview(cycleId: number): Promise { + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId, + stage: ReviewStage.NEW, + }), + ); + return review.id!; + } + + describe('CycleStageNotificationListener', () => { + it('dispatches strategic-report-ready email when cycle is PUBLISHED', async () => { + const cycleId = await makeCycle(); + + await cycleStageListener.handle( + new CycleStageChangedEvent( + cycleId, + CycleStage.PREPARING_REPORT, + CycleStage.PUBLISHED, + ), + ); + + expect(mailer.calls).toHaveLength(1); + expect(mailer.calls[0].template).toBe( + 'cycle-strategic-report-ready', + ); + + const log = await notificationLogRepo.findOneForCycle( + cycleId, + NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + hrId, + ); + expect(log!.isSent()).toBe(true); + }); + + it('does nothing for non-PUBLISHED transitions', async () => { + const cycleId = await makeCycle(); + + await cycleStageListener.handle( + new CycleStageChangedEvent( + cycleId, + CycleStage.NEW, + CycleStage.ACTIVE, + ), + ); + + expect(mailer.calls).toEqual([]); + }); + + it('swallows errors from the dispatch and does not rethrow', async () => { + const cycleId = await makeCycle(); + mailer.shouldFail = true; + + await expect( + cycleStageListener.handle( + new CycleStageChangedEvent( + cycleId, + CycleStage.PREPARING_REPORT, + CycleStage.PUBLISHED, + ), + ), + ).resolves.toBeUndefined(); + }); + }); + + describe('ReviewStageNotificationListener', () => { + it('emails the ratee when review transitions to SELF_ASSESSMENT', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + + await reviewStageListener.handle( + new ReviewStageChangedEvent( + reviewId, + ReviewStage.NEW, + ReviewStage.SELF_ASSESSMENT, + ), + ); + + expect(mailer.calls).toHaveLength(1); + expect(mailer.calls[0].template).toBe('ratee-self-assessment'); + }); + + it('emails respondents when review transitions to IN_PROGRESS', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + // No respondents seeded → notifyRespondents returns 0, no + // mail call, but the handler should still complete. + + await reviewStageListener.handle( + new ReviewStageChangedEvent( + reviewId, + ReviewStage.SELF_ASSESSMENT, + ReviewStage.IN_PROGRESS, + ), + ); + + expect(mailer.calls).toEqual([]); + }); + + it('emails HR when review transitions to PROCESSING_BY_HR', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + + await reviewStageListener.handle( + new ReviewStageChangedEvent( + reviewId, + ReviewStage.PREPARING_REPORT, + ReviewStage.PROCESSING_BY_HR, + ), + ); + + expect(mailer.calls).toHaveLength(1); + expect(mailer.calls[0].template).toBe('hr-report-ready'); + }); + + it('emails reviewers when review transitions to PUBLISHED', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + // No reviewers seeded → service returns 0, no mail. + + await reviewStageListener.handle( + new ReviewStageChangedEvent( + reviewId, + ReviewStage.PROCESSING_BY_HR, + ReviewStage.PUBLISHED, + ), + ); + + expect(mailer.calls).toEqual([]); + }); + + it('does nothing for transitions not in the switch', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + + await reviewStageListener.handle( + new ReviewStageChangedEvent( + reviewId, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ), + ); + + expect(mailer.calls).toEqual([]); + }); + }); + + describe('UserCreatedNotificationListener', () => { + it('dispatches a welcome email when a user-created event fires', async () => { + await userCreatedListener.handle( + new UserCreatedEvent(rateeId, 'ratee@example.com'), + ); + + expect(mailer.calls).toHaveLength(1); + expect(mailer.calls[0].template).toBe('user-welcome'); + + const log = await notificationLogRepo.findOneForUser( + NotificationKind.USER_WELCOME, + rateeId, + ); + expect(log!.isSent()).toBe(true); + }); + + it('logs and swallows errors from the dispatch', async () => { + mailer.shouldFail = true; + + await expect( + userCreatedListener.handle( + new UserCreatedEvent(rateeId, 'ratee@example.com'), + ), + ).resolves.toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/notifications/repository/notification-log.repository.integration-spec.ts b/apps/api/test/integration/contexts/notifications/repository/notification-log.repository.integration-spec.ts new file mode 100644 index 00000000..be94d35d --- /dev/null +++ b/apps/api/test/integration/contexts/notifications/repository/notification-log.repository.integration-spec.ts @@ -0,0 +1,379 @@ +import '../../../setup-env'; + +import { ReviewStage } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { NotificationChannel } from 'src/contexts/notifications/domain/notification-channel.enum'; +import { NotificationKind } from 'src/contexts/notifications/domain/notification-kind.enum'; +import { NotificationLogDomain } from 'src/contexts/notifications/domain/notification-log.domain'; +import { NotificationLogRepository } from 'src/contexts/notifications/infrastructure/prisma-repositories/notification-log.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createNotificationsTestModule, + resetNotificationTables, +} from '../test-app-notifications'; + +describe('NotificationLogRepository (integration)', () => { + let module: TestingModule; + let repo: NotificationLogRepository; + let cycles: CycleRepository; + let reviews: ReviewRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let recipientUserId: number; + let hrId: number; + let positionId: number; + + beforeAll(async () => { + module = await createNotificationsTestModule(); + repo = module.get(NotificationLogRepository); + cycles = module.get(CycleRepository); + reviews = module.get(ReviewRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetNotificationTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const recipient = await identityUsers.create({ + firstName: 'Recipient', + lastName: 'Person', + email: `recipient.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + + hrId = hr.id!; + recipientUserId = recipient.id!; + positionId = position.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function makeCycle(): Promise { + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + return cycle.id!; + } + + async function makeReview(cycleId: number): Promise { + const ratee = await identityUsers.create({ + firstName: 'Ratee', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Ratee Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId, + stage: ReviewStage.NEW, + }), + ); + return review.id!; + } + + function buildLog( + overrides: Partial<{ + reviewId: number | null; + cycleId: number | null; + kind: NotificationKind; + recipientEmail: string; + recipientUserId: number; + }> = {}, + ): NotificationLogDomain { + return NotificationLogDomain.create({ + reviewId: overrides.reviewId ?? null, + cycleId: overrides.cycleId ?? null, + kind: overrides.kind ?? NotificationKind.USER_WELCOME, + recipientUserId: overrides.recipientUserId ?? recipientUserId, + recipientEmail: overrides.recipientEmail ?? 'r@example.com', + }); + } + + describe('create', () => { + it('persists a log with channel EMAIL by default', async () => { + const created = await repo.create(buildLog()); + + expect(created.id).toBeDefined(); + expect(created.channel).toBe(NotificationChannel.EMAIL); + expect(created.kind).toBe(NotificationKind.USER_WELCOME); + expect(created.sentAt).toBeNull(); + expect(created.errorMessage).toBeNull(); + + const fromDb = await prisma.notificationLog.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.recipientUserId).toBe(recipientUserId); + }); + + it('persists a log tied to a review', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + + const created = await repo.create( + buildLog({ + reviewId, + kind: NotificationKind.RATEE_SELF_ASSESSMENT, + }), + ); + + expect(created.reviewId).toBe(reviewId); + }); + + it('persists a log tied to a cycle', async () => { + const cycleId = await makeCycle(); + + const created = await repo.create( + buildLog({ + cycleId, + kind: NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + }), + ); + + expect(created.cycleId).toBe(cycleId); + }); + }); + + describe('findOneForReview', () => { + it('returns the log matching (reviewId, kind, recipientUserId)', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + await repo.create( + buildLog({ + reviewId, + kind: NotificationKind.RATEE_SELF_ASSESSMENT, + }), + ); + + const fetched = await repo.findOneForReview( + reviewId, + NotificationKind.RATEE_SELF_ASSESSMENT, + recipientUserId, + ); + + expect(fetched).toBeInstanceOf(NotificationLogDomain); + expect(fetched!.reviewId).toBe(reviewId); + }); + + it('returns null when no match exists', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + + await expect( + repo.findOneForReview( + reviewId, + NotificationKind.RATEE_SELF_ASSESSMENT, + recipientUserId, + ), + ).resolves.toBeNull(); + }); + + it('enforces the unique (reviewId, kind, recipientUserId) constraint', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + await repo.create( + buildLog({ + reviewId, + kind: NotificationKind.RATEE_SELF_ASSESSMENT, + }), + ); + + await expect( + repo.create( + buildLog({ + reviewId, + kind: NotificationKind.RATEE_SELF_ASSESSMENT, + }), + ), + ).rejects.toThrow(); + }); + }); + + describe('findOneForCycle', () => { + it('returns the log matching (cycleId, kind, recipientUserId)', async () => { + const cycleId = await makeCycle(); + await repo.create( + buildLog({ + cycleId, + kind: NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + }), + ); + + const fetched = await repo.findOneForCycle( + cycleId, + NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + recipientUserId, + ); + + expect(fetched).toBeInstanceOf(NotificationLogDomain); + expect(fetched!.cycleId).toBe(cycleId); + }); + + it('returns null when no match exists', async () => { + const cycleId = await makeCycle(); + + await expect( + repo.findOneForCycle( + cycleId, + NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + recipientUserId, + ), + ).resolves.toBeNull(); + }); + + it('enforces the unique (cycleId, kind, recipientUserId) constraint', async () => { + const cycleId = await makeCycle(); + await repo.create( + buildLog({ + cycleId, + kind: NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + }), + ); + + await expect( + repo.create( + buildLog({ + cycleId, + kind: NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + }), + ), + ).rejects.toThrow(); + }); + }); + + describe('findOneForUser', () => { + it('returns the welcome-style log for the user (cycleId/reviewId both null)', async () => { + await repo.create( + buildLog({ kind: NotificationKind.USER_WELCOME }), + ); + + const fetched = await repo.findOneForUser( + NotificationKind.USER_WELCOME, + recipientUserId, + ); + + expect(fetched).toBeInstanceOf(NotificationLogDomain); + expect(fetched!.reviewId).toBeNull(); + expect(fetched!.cycleId).toBeNull(); + }); + + it('does not match logs that have a reviewId or cycleId set', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + // Different kind, attached to a review + await repo.create( + buildLog({ + reviewId, + kind: NotificationKind.RATEE_SELF_ASSESSMENT, + }), + ); + + await expect( + repo.findOneForUser( + NotificationKind.USER_WELCOME, + recipientUserId, + ), + ).resolves.toBeNull(); + }); + + it('returns null when there is no welcome log', async () => { + await expect( + repo.findOneForUser( + NotificationKind.USER_WELCOME, + recipientUserId, + ), + ).resolves.toBeNull(); + }); + }); + + describe('markSent', () => { + it('sets sentAt to a non-null value and clears errorMessage', async () => { + const created = await repo.create(buildLog()); + // Pre-set an error message to verify it gets cleared. + await repo.markFailure(created.id!, 'temporary failure'); + + await repo.markSent(created.id!); + + const fromDb = await prisma.notificationLog.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.sentAt).not.toBeNull(); + expect(fromDb!.errorMessage).toBeNull(); + }); + }); + + describe('markFailure', () => { + it('persists the error message', async () => { + const created = await repo.create(buildLog()); + + await repo.markFailure(created.id!, 'SMTP timed out'); + + const fromDb = await prisma.notificationLog.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.errorMessage).toBe('SMTP timed out'); + expect(fromDb!.sentAt).toBeNull(); + }); + }); + + describe('cascade behaviour', () => { + it('cascade-deletes logs when their cycle is removed', async () => { + const cycleId = await makeCycle(); + const log = await repo.create( + buildLog({ + cycleId, + kind: NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + }), + ); + + await prisma.cycle.delete({ where: { id: cycleId } }); + + const fromDb = await prisma.notificationLog.findUnique({ + where: { id: log.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); + + describe('isSent (domain)', () => { + it('flips from false to true once persisted with sentAt', async () => { + const created = await repo.create(buildLog()); + expect(created.isSent()).toBe(false); + + await repo.markSent(created.id!); + const reloaded = await repo.findOneForUser( + NotificationKind.USER_WELCOME, + recipientUserId, + ); + + expect(reloaded!.isSent()).toBe(true); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/notifications/service/review-email-notification.service.integration-spec.ts b/apps/api/test/integration/contexts/notifications/service/review-email-notification.service.integration-spec.ts new file mode 100644 index 00000000..c9e4fb7b --- /dev/null +++ b/apps/api/test/integration/contexts/notifications/service/review-email-notification.service.integration-spec.ts @@ -0,0 +1,307 @@ +import '../../../setup-env'; + +import { + RespondentCategory, + ResponseStatus, + ReviewStage, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { RespondentRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { ReviewerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/reviewer.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { ReviewEmailNotificationService } from 'src/contexts/notifications/application/services/review-email-notification.service'; +import { NotificationKind } from 'src/contexts/notifications/domain/notification-kind.enum'; +import { NotificationLogRepository } from 'src/contexts/notifications/infrastructure/prisma-repositories/notification-log.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + MailerStub, + createNotificationsServiceTestModule, + resetNotificationTables, +} from '../test-app-notifications'; + +describe('ReviewEmailNotificationService (integration)', () => { + let module: TestingModule; + let mailer: MailerStub; + let service: ReviewEmailNotificationService; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let respondentRepo: RespondentRepository; + let reviewerRepo: ReviewerRepository; + let positionRepo: PositionRepository; + let notificationLogRepo: NotificationLogRepository; + let identityUsers: IdentityUserService; + let userRepo: UserRepository; + let prisma: PrismaService; + + let hrId: number; + let rateeId: number; + let positionId: number; + + beforeAll(async () => { + const built = await createNotificationsServiceTestModule(); + module = built.module; + mailer = built.mailerStub; + service = module.get(ReviewEmailNotificationService); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + respondentRepo = module.get(RespondentRepository); + reviewerRepo = module.get(ReviewerRepository); + positionRepo = module.get(PositionRepository); + notificationLogRepo = module.get(NotificationLogRepository); + identityUsers = module.get(IdentityUserService); + userRepo = module.get(UserRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetNotificationTables(prisma); + mailer.reset(); + // Seed users via the repo directly (NOT via IdentityUserService) + // because the latter emits `user.created`, which fires the + // UserCreatedNotificationListener and pollutes `mailer.calls` + // before each test's body runs. + const hr = await userRepo.create( + UserDomain.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + }), + ); + const ratee = await userRepo.create( + UserDomain.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + }), + ); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function makeCycle(): Promise { + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Q1 2026', + hrId, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + return cycle.id!; + } + + async function makeReview(cycleId: number): Promise { + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId, + stage: ReviewStage.NEW, + }), + ); + return review.id!; + } + + describe('notifyUserWelcome', () => { + it('sends a welcome email and persists a sent NotificationLog row', async () => { + const sentCount = await service.notifyUserWelcome(rateeId); + + expect(sentCount).toBe(1); + expect(mailer.calls).toHaveLength(1); + expect(mailer.calls[0].template).toBe('user-welcome'); + expect(mailer.calls[0].subject).toContain('Welcome'); + + const log = await notificationLogRepo.findOneForUser( + NotificationKind.USER_WELCOME, + rateeId, + ); + expect(log).not.toBeNull(); + expect(log!.isSent()).toBe(true); + }); + + it('skips dispatch when the user already has a sent welcome log', async () => { + await service.notifyUserWelcome(rateeId); + mailer.reset(); + + const sentCount = await service.notifyUserWelcome(rateeId); + + expect(sentCount).toBe(0); + expect(mailer.calls).toEqual([]); + }); + + it('returns 0 when the user is missing', async () => { + const sentCount = await service.notifyUserWelcome(999999); + + expect(sentCount).toBe(0); + expect(mailer.calls).toEqual([]); + }); + }); + + describe('notifyRateeSelfAssessment', () => { + it('sends one email to the ratee and writes a sent log tied to the review', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + + const sentCount = await service.notifyRateeSelfAssessment(reviewId); + + expect(sentCount).toBe(1); + expect(mailer.calls).toHaveLength(1); + expect(mailer.calls[0].template).toBe('ratee-self-assessment'); + + const log = await notificationLogRepo.findOneForReview( + reviewId, + NotificationKind.RATEE_SELF_ASSESSMENT, + rateeId, + ); + expect(log!.isSent()).toBe(true); + }); + + it('returns 0 when the review is missing', async () => { + const sentCount = await service.notifyRateeSelfAssessment(999999); + + expect(sentCount).toBe(0); + expect(mailer.calls).toEqual([]); + }); + }); + + describe('notifyRespondents', () => { + it('emails every respondent and writes one log row per recipient', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + const sara = await userRepo.create( + UserDomain.create({ + firstName: 'Sara', + lastName: 'Lopez', + email: `sara.${Date.now()}.${Math.random()}@example.com`, + }), + ); + const ben = await userRepo.create( + UserDomain.create({ + firstName: 'Ben', + lastName: 'Cole', + email: `ben.${Date.now()}.${Math.random()}@example.com`, + }), + ); + for (const respondent of [sara, ben]) { + await respondentRepo.create( + RespondentDomain.create({ + reviewId, + respondentId: respondent.id!, + fullName: respondent.fullName, + category: RespondentCategory.TEAM, + responseStatus: ResponseStatus.PENDING, + positionId, + positionTitle: 'Engineer', + }), + ); + } + + const sentCount = await service.notifyRespondents(reviewId); + + expect(sentCount).toBe(2); + expect(mailer.calls.map((c) => c.to).sort()).toEqual( + [sara.email, ben.email].sort(), + ); + + const saraLog = await notificationLogRepo.findOneForReview( + reviewId, + NotificationKind.RESPONDENT_INVITATION, + sara.id!, + ); + expect(saraLog!.isSent()).toBe(true); + }); + + it('returns 0 when the review is missing', async () => { + const sentCount = await service.notifyRespondents(999999); + expect(sentCount).toBe(0); + }); + }); + + describe('notifyHrReportReady', () => { + it('emails the HR for the review and persists a sent log', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + + const sentCount = await service.notifyHrReportReady(reviewId); + + expect(sentCount).toBe(1); + expect(mailer.calls[0].template).toBe('hr-report-ready'); + + const log = await notificationLogRepo.findOneForReview( + reviewId, + NotificationKind.HR_REPORT_READY, + hrId, + ); + expect(log!.isSent()).toBe(true); + }); + }); + + describe('notifyReviewers', () => { + it('emails every reviewer attached to the review', async () => { + const cycleId = await makeCycle(); + const reviewId = await makeReview(cycleId); + const reviewerUser = await userRepo.create( + UserDomain.create({ + firstName: 'Reviewer', + lastName: 'Person', + email: `rev.${Date.now()}.${Math.random()}@example.com`, + }), + ); + await reviewerRepo.create( + ReviewerDomain.create({ + reviewId, + reviewerId: reviewerUser.id!, + fullName: reviewerUser.fullName, + positionId, + positionTitle: 'Engineer', + }), + ); + + const sentCount = await service.notifyReviewers(reviewId); + + expect(sentCount).toBe(1); + expect(mailer.calls[0].template).toBe('reviewer-report-ready'); + }); + }); + + describe('failure handling', () => { + it('records the error message and reports 0 sent when the mailer throws', async () => { + mailer.shouldFail = true; + mailer.failureMessage = 'SMTP timed out'; + + const sentCount = await service.notifyUserWelcome(rateeId); + + expect(sentCount).toBe(0); + + const log = await notificationLogRepo.findOneForUser( + NotificationKind.USER_WELCOME, + rateeId, + ); + expect(log).not.toBeNull(); + expect(log!.isSent()).toBe(false); + expect(log!.errorMessage).toBe('SMTP timed out'); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/notifications/test-app-notifications.ts b/apps/api/test/integration/contexts/notifications/test-app-notifications.ts new file mode 100644 index 00000000..f4684e58 --- /dev/null +++ b/apps/api/test/integration/contexts/notifications/test-app-notifications.ts @@ -0,0 +1,241 @@ +import { ConfigModule } from '@nestjs/config'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { Test, TestingModule } from '@nestjs/testing'; +import appConfig from 'src/config/app'; +import databaseConfig from 'src/config/database'; +import { CYCLE_STAGE_HISTORY_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle-stage-history.repository.port'; +import { CYCLE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle.repository.port'; +import { RESPONDENT_REPOSITORY } from 'src/contexts/feedback360/application/ports/respondent.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { REVIEWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/reviewer.repository.port'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { CycleStageHistoryRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { RespondentRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { ReviewerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/reviewer.repository'; +import { IDENTITY_ROLE_REPOSITORY } from 'src/contexts/identity/application/ports/role.repository.port'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { RoleRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/role.repository'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { CycleStageNotificationListener } from 'src/contexts/notifications/application/listeners/cycle-stage-notification.listener'; +import { ReviewStageNotificationListener } from 'src/contexts/notifications/application/listeners/review-stage-notification.listener'; +import { UserCreatedNotificationListener } from 'src/contexts/notifications/application/listeners/user-created-notification.listener'; +import { + MAILER, + MailerPort, + MailerSendOptions, +} from 'src/contexts/notifications/application/ports/mailer.port'; +import { NOTIFICATION_LOG_REPOSITORY } from 'src/contexts/notifications/application/ports/notification-log.repository.port'; +import { ReviewEmailNotificationService } from 'src/contexts/notifications/application/services/review-email-notification.service'; +import { NotificationLogRepository } from 'src/contexts/notifications/infrastructure/prisma-repositories/notification-log.repository'; +import { ORGANISATION_POSITION_REPOSITORY } from 'src/contexts/organisation/application/ports/position.repository.port'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { STRATEGIC_REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report.repository.port'; +import { DatabaseModule } from 'src/database/database.module'; +import { PrismaService } from 'src/database/prisma.service'; +import { ensureTestDatabaseExists } from '../../setup-env'; + +/** + * In-memory mailer that records every `sendMail()` invocation. Tests + * inspect `calls` to assert that the right templates / recipients went + * out, and toggle `shouldFail` to drive the failure branch. + */ +export class MailerStub implements MailerPort { + readonly calls: MailerSendOptions[] = []; + shouldFail = false; + failureMessage = 'SMTP failure (stub)'; + + async sendMail(options: MailerSendOptions): Promise { + this.calls.push(options); + if (this.shouldFail) { + throw new Error(this.failureMessage); + } + } + + reset(): void { + this.calls.length = 0; + this.shouldFail = false; + } +} + +/** + * Bootstraps a Nest testing module for the notifications context wired + * to the real database. We assemble providers manually instead of + * importing `NotificationsModule` because that module pulls in + * `MailerModule` (Gmail OAuth2), `Feedback360Module`, `ReportingModule` + * and `IdentityModule` — none of which are needed for repository-level + * tests against `NotificationLog`. + * + * Cross-context dependencies wired in: + * - Identity (`IdentityUserService`) so notification log entries can + * reference real `identity_users` rows for `recipientUserId`. + * - Feedback360 (`CycleRepository`, `ReviewRepository`) so logs can + * reference real cycles and reviews for FK / cascade behaviour. + * - Organisation (`PositionRepository`) because reviews require a + * real position id. + */ +export async function createNotificationsTestModule(): Promise { + await ensureTestDatabaseExists(); + + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig], + ignoreEnvFile: true, + }), + EventEmitterModule.forRoot(), + DatabaseModule, + ], + providers: [ + // Notifications + NotificationLogRepository, + { + provide: NOTIFICATION_LOG_REPOSITORY, + useExisting: NotificationLogRepository, + }, + // Identity + IdentityUserService, + IdentityRoleService, + UserRepository, + RoleRepository, + { provide: IDENTITY_USER_REPOSITORY, useExisting: UserRepository }, + { provide: IDENTITY_ROLE_REPOSITORY, useExisting: RoleRepository }, + // Organisation + PositionRepository, + { + provide: ORGANISATION_POSITION_REPOSITORY, + useExisting: PositionRepository, + }, + // Feedback360 + CycleRepository, + ReviewRepository, + { provide: CYCLE_REPOSITORY, useExisting: CycleRepository }, + { provide: REVIEW_REPOSITORY, useExisting: ReviewRepository }, + ], + }).compile(); + + await module.init(); + return module; +} + +/** + * Bootstraps a richer test module that includes + * `ReviewEmailNotificationService`. Returns the testing module plus a + * `MailerStub` instance — tests use the stub to assert outbound mail + * calls without standing up a real SMTP transport. The `STRATEGIC_REPORT_REPOSITORY` + * is wired with an inline stub because we don't need the real reporting + * context here. + */ +export async function createNotificationsServiceTestModule(): Promise<{ + module: TestingModule; + mailerStub: MailerStub; +}> { + await ensureTestDatabaseExists(); + + const mailerStub = new MailerStub(); + const strategicReportStub = { + async findByCycleId() { + return null; + }, + async create() { + throw new Error('not implemented in test stub'); + }, + async findById() { + return null; + }, + async search() { + return []; + }, + }; + + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig], + ignoreEnvFile: true, + }), + EventEmitterModule.forRoot(), + DatabaseModule, + ], + providers: [ + // Notifications + ReviewEmailNotificationService, + CycleStageNotificationListener, + ReviewStageNotificationListener, + UserCreatedNotificationListener, + NotificationLogRepository, + { + provide: NOTIFICATION_LOG_REPOSITORY, + useExisting: NotificationLogRepository, + }, + { provide: MAILER, useValue: mailerStub }, + // Identity + IdentityUserService, + IdentityRoleService, + UserRepository, + RoleRepository, + { provide: IDENTITY_USER_REPOSITORY, useExisting: UserRepository }, + { provide: IDENTITY_ROLE_REPOSITORY, useExisting: RoleRepository }, + // Organisation + PositionRepository, + { + provide: ORGANISATION_POSITION_REPOSITORY, + useExisting: PositionRepository, + }, + // Feedback360 + CycleService, + CycleRepository, + ReviewRepository, + RespondentRepository, + ReviewerRepository, + CycleStageHistoryRepository, + { provide: CYCLE_REPOSITORY, useExisting: CycleRepository }, + { provide: REVIEW_REPOSITORY, useExisting: ReviewRepository }, + { + provide: RESPONDENT_REPOSITORY, + useExisting: RespondentRepository, + }, + { provide: REVIEWER_REPOSITORY, useExisting: ReviewerRepository }, + { + provide: CYCLE_STAGE_HISTORY_REPOSITORY, + useExisting: CycleStageHistoryRepository, + }, + // Reporting — stub + { + provide: STRATEGIC_REPORT_REPOSITORY, + useValue: strategicReportStub, + }, + ], + }).compile(); + + await module.init(); + return { module, mailerStub }; +} + +/** + * Truncates the notification_logs table plus every cross-context table + * notifications scenarios touch. The order is not significant because + * we use CASCADE. `identity_roles` (reference data) is preserved. + */ +export async function resetNotificationTables( + prisma: PrismaService, +): Promise { + await prisma.$executeRawUnsafe( + `TRUNCATE TABLE + "notification_logs", + "feedback360_cycle_stage_history", + "feedback360_respondents", + "feedback360_reviewers", + "feedback360_reviews", + "feedback360_cycles", + "org_positions", + "identity_users_roles", + "identity_users" + RESTART IDENTITY CASCADE`, + ); +} diff --git a/apps/api/test/integration/contexts/organisation/repository/position-hierarchy.repository.integration-spec.ts b/apps/api/test/integration/contexts/organisation/repository/position-hierarchy.repository.integration-spec.ts new file mode 100644 index 00000000..7b0e4431 --- /dev/null +++ b/apps/api/test/integration/contexts/organisation/repository/position-hierarchy.repository.integration-spec.ts @@ -0,0 +1,125 @@ +import '../../../setup-env'; + +import { TestingModule } from '@nestjs/testing'; +import { PositionHierarchyDomain } from 'src/contexts/organisation/domain/position-hierarchy.domain'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionHierarchyRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position-hierarchy.repository'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createOrganisationTestModule, + resetOrganisationTables, +} from '../test-app-organisation'; + +describe('PositionHierarchyRepository (integration)', () => { + let module: TestingModule; + let repo: PositionHierarchyRepository; + let positions: PositionRepository; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createOrganisationTestModule(); + repo = module.get(PositionHierarchyRepository); + positions = module.get(PositionRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetOrganisationTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + async function makePositions() { + const cto = await positions.create( + PositionDomain.create({ title: 'CTO' }), + ); + const lead = await positions.create( + PositionDomain.create({ title: 'Tech Lead' }), + ); + return { cto, lead }; + } + + describe('link', () => { + it('persists a hierarchy relation', async () => { + const { cto, lead } = await makePositions(); + + const created = await repo.link(cto.id!, lead.id!); + + expect(created).toBeInstanceOf(PositionHierarchyDomain); + expect(created.superiorPositionId).toBe(cto.id); + expect(created.subordinatePositionId).toBe(lead.id); + }); + + it('is idempotent: returns the existing row on duplicate link', async () => { + const { cto, lead } = await makePositions(); + + const first = await repo.link(cto.id!, lead.id!); + const second = await repo.link(cto.id!, lead.id!); + + expect(second.id).toBe(first.id); + + const rows = await prisma.positionHierarchy.findMany({ + where: { + superiorPositionId: cto.id!, + subordinatePositionId: lead.id!, + }, + }); + expect(rows).toHaveLength(1); + }); + }); + + describe('unlink', () => { + it('removes the relation when it exists', async () => { + const { cto, lead } = await makePositions(); + await repo.link(cto.id!, lead.id!); + + await repo.unlink(cto.id!, lead.id!); + + const rows = await prisma.positionHierarchy.findMany({ + where: { + superiorPositionId: cto.id!, + subordinatePositionId: lead.id!, + }, + }); + expect(rows).toEqual([]); + }); + + it('does not throw when the relation is absent', async () => { + const { cto, lead } = await makePositions(); + await expect( + repo.unlink(cto.id!, lead.id!), + ).resolves.toBeUndefined(); + }); + }); + + describe('listSubordinates / listSuperiors', () => { + it('listSubordinates returns relations where the position is superior', async () => { + const { cto, lead } = await makePositions(); + await repo.link(cto.id!, lead.id!); + + const result = await repo.listSubordinates(cto.id!); + + expect(result).toHaveLength(1); + expect(result[0].subordinatePositionId).toBe(lead.id); + }); + + it('listSuperiors returns relations where the position is subordinate', async () => { + const { cto, lead } = await makePositions(); + await repo.link(cto.id!, lead.id!); + + const result = await repo.listSuperiors(lead.id!); + + expect(result).toHaveLength(1); + expect(result[0].superiorPositionId).toBe(cto.id); + }); + + it('returns an empty array when no relations exist', async () => { + const { cto } = await makePositions(); + await expect(repo.listSubordinates(cto.id!)).resolves.toEqual([]); + await expect(repo.listSuperiors(cto.id!)).resolves.toEqual([]); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/organisation/repository/position.repository.integration-spec.ts b/apps/api/test/integration/contexts/organisation/repository/position.repository.integration-spec.ts new file mode 100644 index 00000000..18f1c434 --- /dev/null +++ b/apps/api/test/integration/contexts/organisation/repository/position.repository.integration-spec.ts @@ -0,0 +1,148 @@ +import '../../../setup-env'; + +import { PositionSortField, SortDirection } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createOrganisationTestModule, + resetOrganisationTables, +} from '../test-app-organisation'; + +describe('PositionRepository (integration)', () => { + let module: TestingModule; + let repo: PositionRepository; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createOrganisationTestModule(); + repo = module.get(PositionRepository); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetOrganisationTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a position to the database', async () => { + const created = await repo.create( + PositionDomain.create({ + title: 'Engineer', + description: 'Software engineer', + }), + ); + + expect(created.id).toBeDefined(); + const fromDb = await prisma.position.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Engineer'); + expect(fromDb!.description).toBe('Software engineer'); + }); + }); + + describe('findById', () => { + it('returns the position when found', async () => { + const created = await repo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + + await expect(repo.findById(created.id!)).resolves.toMatchObject({ + id: created.id, + title: 'Engineer', + }); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await repo.create( + PositionDomain.create({ + title: 'Senior Engineer', + description: 'Backend', + }), + ); + await repo.create( + PositionDomain.create({ + title: 'Junior Engineer', + description: 'Backend', + }), + ); + await repo.create( + PositionDomain.create({ + title: 'Product Manager', + description: 'Product', + }), + ); + }); + + it('returns all positions when no filter is supplied', async () => { + const all = await repo.search({} as any); + expect(all).toHaveLength(3); + }); + + it('filters by title (case insensitive substring)', async () => { + const result = await repo.search({ title: 'engineer' } as any); + expect(result.map((p) => p.title).sort()).toEqual([ + 'Junior Engineer', + 'Senior Engineer', + ]); + }); + + it('honours descending sort direction on title', async () => { + const result = await repo.search({ + sortBy: PositionSortField.TITLE, + sortDirection: SortDirection.DESC, + } as any); + + const titles = result.map((p) => p.title); + expect(titles).toEqual([...titles].sort().reverse()); + }); + }); + + describe('updateById', () => { + it('persists the patched fields', async () => { + const created = await repo.create( + PositionDomain.create({ + title: 'Engineer', + description: 'old', + }), + ); + + const updated = await repo.updateById(created.id!, { + description: 'new', + }); + + expect(updated.description).toBe('new'); + const fromDb = await prisma.position.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.description).toBe('new'); + }); + }); + + describe('deleteById', () => { + it('removes the position row', async () => { + const created = await repo.create( + PositionDomain.create({ title: 'Doomed' }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.position.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/organisation/repository/team.repository.integration-spec.ts b/apps/api/test/integration/contexts/organisation/repository/team.repository.integration-spec.ts new file mode 100644 index 00000000..c4b98d97 --- /dev/null +++ b/apps/api/test/integration/contexts/organisation/repository/team.repository.integration-spec.ts @@ -0,0 +1,217 @@ +import '../../../setup-env'; + +import { SortDirection, TeamSortField } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { TeamMembershipDomain } from 'src/contexts/organisation/domain/team-membership.domain'; +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; +import { TeamRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/team.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createOrganisationTestModule, + resetOrganisationTables, +} from '../test-app-organisation'; + +describe('TeamRepository (integration)', () => { + let module: TestingModule; + let repo: TeamRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let alice: UserDomain; + let bob: UserDomain; + + beforeAll(async () => { + module = await createOrganisationTestModule(); + repo = module.get(TeamRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetOrganisationTables(prisma); + alice = await identityUsers.create({ + firstName: 'Alice', + lastName: 'Brown', + email: `alice.${Date.now()}@example.com`, + } as any); + bob = await identityUsers.create({ + firstName: 'Bob', + lastName: 'Green', + email: `bob.${Date.now()}@example.com`, + } as any); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a team without a head', async () => { + const created = await repo.create( + TeamDomain.create({ title: 'Platform' }), + ); + + expect(created.id).toBeDefined(); + expect(created.headId).toBeNull(); + + const fromDb = await prisma.team.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.title).toBe('Platform'); + }); + + it('persists a team with a head pointing to a real user', async () => { + const created = await repo.create( + TeamDomain.create({ + title: 'Platform', + headId: alice.id!, + }), + ); + + expect(created.headId).toBe(alice.id); + }); + }); + + describe('findById / search', () => { + beforeEach(async () => { + await repo.create(TeamDomain.create({ title: 'Alpha' })); + await repo.create(TeamDomain.create({ title: 'Beta' })); + await repo.create( + TeamDomain.create({ + title: 'Gamma', + headId: alice.id!, + }), + ); + }); + + it('findById returns the team when found', async () => { + const all = await repo.search({} as any); + const target = all[0]; + + await expect(repo.findById(target.id!)).resolves.toBeInstanceOf( + TeamDomain, + ); + }); + + it('findById returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + + it('search filters by headId', async () => { + const result = await repo.search({ headId: alice.id! } as any); + + expect(result).toHaveLength(1); + expect(result[0].title).toBe('Gamma'); + }); + + it('search supports descending sort on title', async () => { + const result = await repo.search({ + sortBy: TeamSortField.TITLE, + sortDirection: SortDirection.DESC, + } as any); + + const titles = result.map((t) => t.title); + expect(titles).toEqual([...titles].sort().reverse()); + }); + }); + + describe('updateById', () => { + it('persists the patched fields', async () => { + const created = await repo.create( + TeamDomain.create({ title: 'Platform' }), + ); + + const updated = await repo.updateById(created.id!, { + description: 'new desc', + }); + + expect(updated.description).toBe('new desc'); + }); + }); + + describe('deleteById', () => { + it('removes the team row', async () => { + const created = await repo.create( + TeamDomain.create({ title: 'Doomed' }), + ); + + await repo.deleteById(created.id!); + + const fromDb = await prisma.team.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + }); + + describe('addMember / removeMember / listMembers', () => { + it('addMember creates the membership row', async () => { + const team = await repo.create( + TeamDomain.create({ title: 'Platform' }), + ); + + const membership = await repo.addMember(team.id!, alice.id!, true); + + expect(membership).toBeInstanceOf(TeamMembershipDomain); + expect(membership.teamId).toBe(team.id); + expect(membership.memberId).toBe(alice.id); + expect(membership.isPrimary).toBe(true); + }); + + it('addMember is idempotent (returns existing on unique conflict)', async () => { + const team = await repo.create( + TeamDomain.create({ title: 'Platform' }), + ); + + const first = await repo.addMember(team.id!, alice.id!, true); + const second = await repo.addMember(team.id!, alice.id!, true); + + expect(second.id).toBe(first.id); + + const rows = await prisma.teamMembership.findMany({ + where: { teamId: team.id! }, + }); + expect(rows).toHaveLength(1); + }); + + it('coerces a missing isPrimary to false', async () => { + const team = await repo.create( + TeamDomain.create({ title: 'Platform' }), + ); + + const membership = await repo.addMember(team.id!, alice.id!); + + expect(membership.isPrimary).toBe(false); + }); + + it('listMembers returns all memberships for the team', async () => { + const team = await repo.create( + TeamDomain.create({ title: 'Platform' }), + ); + await repo.addMember(team.id!, alice.id!, true); + await repo.addMember(team.id!, bob.id!, false); + + const result = await repo.listMembers(team.id!); + + expect(result).toHaveLength(2); + expect(result.map((m) => m.memberId).sort()).toEqual( + [alice.id!, bob.id!].sort(), + ); + }); + + it('removeMember deletes the membership row', async () => { + const team = await repo.create( + TeamDomain.create({ title: 'Platform' }), + ); + await repo.addMember(team.id!, alice.id!, true); + + await repo.removeMember(team.id!, alice.id!); + + const rows = await prisma.teamMembership.findMany({ + where: { teamId: team.id! }, + }); + expect(rows).toEqual([]); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/organisation/service/position-hierarchy.service.integration-spec.ts b/apps/api/test/integration/contexts/organisation/service/position-hierarchy.service.integration-spec.ts new file mode 100644 index 00000000..55531c3a --- /dev/null +++ b/apps/api/test/integration/contexts/organisation/service/position-hierarchy.service.integration-spec.ts @@ -0,0 +1,151 @@ +import '../../../setup-env'; + +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { PositionHierarchyService } from 'src/contexts/organisation/application/services/position-hierarchy.service'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createOrganisationTestModule, + resetOrganisationTables, +} from '../test-app-organisation'; + +describe('PositionHierarchyService (integration)', () => { + let module: TestingModule; + let service: PositionHierarchyService; + let positions: PositionService; + let prisma: PrismaService; + + let cto: PositionDomain; + let lead: PositionDomain; + let engineer: PositionDomain; + + beforeAll(async () => { + module = await createOrganisationTestModule(); + service = module.get(PositionHierarchyService); + positions = module.get(PositionService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetOrganisationTables(prisma); + cto = await positions.create({ title: 'CTO' }); + lead = await positions.create({ title: 'Tech Lead' }); + engineer = await positions.create({ title: 'Engineer' }); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('link', () => { + it('persists a superior → subordinate relation', async () => { + await service.link(cto.id!, lead.id!); + + const rows = await prisma.positionHierarchy.findMany({ + where: { + superiorPositionId: cto.id!, + subordinatePositionId: lead.id!, + }, + }); + expect(rows).toHaveLength(1); + }); + + it('rejects self-linking', async () => { + await expect(service.link(cto.id!, cto.id!)).rejects.toBeInstanceOf( + BadRequestException, + ); + + const rows = await prisma.positionHierarchy.findMany({ + where: { superiorPositionId: cto.id! }, + }); + expect(rows).toEqual([]); + }); + + it('throws NotFoundException when either position is missing', async () => { + await expect(service.link(999999, lead.id!)).rejects.toBeInstanceOf( + NotFoundException, + ); + await expect(service.link(cto.id!, 999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('is idempotent for duplicate links', async () => { + const first = await service.link(cto.id!, lead.id!); + const second = await service.link(cto.id!, lead.id!); + + expect(second.id).toBe(first.id); + + const rows = await prisma.positionHierarchy.findMany({ + where: { + superiorPositionId: cto.id!, + subordinatePositionId: lead.id!, + }, + }); + expect(rows).toHaveLength(1); + }); + }); + + describe('unlink', () => { + it('removes an existing link', async () => { + await service.link(cto.id!, lead.id!); + + await service.unlink(cto.id!, lead.id!); + + const rows = await prisma.positionHierarchy.findMany({ + where: { + superiorPositionId: cto.id!, + subordinatePositionId: lead.id!, + }, + }); + expect(rows).toEqual([]); + }); + + it('does nothing when the link did not exist (no error)', async () => { + await expect( + service.unlink(cto.id!, lead.id!), + ).resolves.toBeUndefined(); + }); + }); + + describe('listSubordinates', () => { + it('returns every subordinate position of the given superior', async () => { + await service.link(cto.id!, lead.id!); + await service.link(cto.id!, engineer.id!); + + const result = await service.listSubordinates(cto.id!); + + expect(result.map((p) => p.id).sort()).toEqual( + [lead.id, engineer.id].sort(), + ); + result.forEach((p) => expect(p).toBeInstanceOf(PositionDomain)); + }); + + it('returns an empty array when there are no subordinates', async () => { + await expect(service.listSubordinates(cto.id!)).resolves.toEqual( + [], + ); + }); + }); + + describe('listSuperiors', () => { + it('returns every superior of the given subordinate', async () => { + await service.link(cto.id!, engineer.id!); + await service.link(lead.id!, engineer.id!); + + const result = await service.listSuperiors(engineer.id!); + + expect(result.map((p) => p.id).sort()).toEqual( + [cto.id, lead.id].sort(), + ); + }); + + it('returns an empty array when there are no superiors', async () => { + await expect(service.listSuperiors(engineer.id!)).resolves.toEqual( + [], + ); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/organisation/service/position.service.integration-spec.ts b/apps/api/test/integration/contexts/organisation/service/position.service.integration-spec.ts new file mode 100644 index 00000000..c4f48e46 --- /dev/null +++ b/apps/api/test/integration/contexts/organisation/service/position.service.integration-spec.ts @@ -0,0 +1,207 @@ +import '../../../setup-env'; + +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createOrganisationTestModule, + resetOrganisationTables, +} from '../test-app-organisation'; + +describe('PositionService (integration)', () => { + let module: TestingModule; + let service: PositionService; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createOrganisationTestModule(); + service = module.get(PositionService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetOrganisationTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a new position with the supplied attributes', async () => { + const created = await service.create({ + title: 'Engineer', + description: 'Software engineer', + }); + + expect(created.id).toBeDefined(); + expect(created.title).toBe('Engineer'); + expect(created.description).toBe('Software engineer'); + + const fromDb = await prisma.position.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).not.toBeNull(); + expect(fromDb!.title).toBe('Engineer'); + }); + + it('coerces a missing description to null in the database', async () => { + const created = await service.create({ title: 'Manager' }); + + const fromDb = await prisma.position.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.description).toBeNull(); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await service.create({ title: 'Senior Engineer' }); + await service.create({ title: 'Junior Engineer' }); + await service.create({ title: 'Product Manager' }); + }); + + it('returns all positions when no filter is supplied', async () => { + const all = await service.search({} as any); + expect(all).toHaveLength(3); + }); + + it('filters by title (case insensitive substring)', async () => { + const result = await service.search({ title: 'ENGINEER' } as any); + expect(result.map((p) => p.title).sort()).toEqual([ + 'Junior Engineer', + 'Senior Engineer', + ]); + }); + }); + + describe('getById', () => { + it('returns the position when found', async () => { + const created = await service.create({ title: 'Engineer' }); + + await expect(service.getById(created.id!)).resolves.toMatchObject({ + id: created.id, + title: 'Engineer', + }); + }); + + it('throws NotFoundException when missing', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('forwards the patch and persists the changes', async () => { + const created = await service.create({ title: 'Engineer' }); + + const updated = await service.update(created.id!, { + description: 'Updated description', + }); + + expect(updated.description).toBe('Updated description'); + + const fromDb = await prisma.position.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb!.description).toBe('Updated description'); + }); + + it('throws NotFoundException when the position does not exist', async () => { + await expect( + service.update(999999, { title: 'X' }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('delete', () => { + it('removes the position when nothing is linked', async () => { + const created = await service.create({ title: 'Doomed' }); + + await service.delete(created.id!); + + const fromDb = await prisma.position.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('rejects deletion when the position has competence relations', async () => { + const created = await service.create({ title: 'Linked' }); + + // Insert a library_position_competence_relations row directly. + // We don't have a Competence yet, so insert one too. + const competence = await prisma.competence.create({ + data: { title: 'Teamwork' }, + }); + await prisma.positionCompetenceRelation.create({ + data: { + positionId: created.id!, + competenceId: competence.id, + }, + }); + + await expect(service.delete(created.id!)).rejects.toBeInstanceOf( + BadRequestException, + ); + + // Position must still be in DB + const stillThere = await prisma.position.findUnique({ + where: { id: created.id! }, + }); + expect(stillThere).not.toBeNull(); + }); + + it('throws NotFoundException when deleting a missing position', async () => { + await expect(service.delete(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('listCompetences', () => { + it('returns competence ids linked to the position', async () => { + const position = await service.create({ title: 'Engineer' }); + const c1 = await prisma.competence.create({ + data: { title: 'Teamwork' }, + }); + const c2 = await prisma.competence.create({ + data: { title: 'Leadership' }, + }); + await prisma.positionCompetenceRelation.createMany({ + data: [ + { positionId: position.id!, competenceId: c1.id }, + { positionId: position.id!, competenceId: c2.id }, + ], + }); + + const result = await service.listCompetences(position.id!); + + expect(result.sort()).toEqual([c1.id, c2.id].sort()); + }); + + it('returns an empty array when the position has no competences', async () => { + const position = await service.create({ title: 'Engineer' }); + await expect( + service.listCompetences(position.id!), + ).resolves.toEqual([]); + }); + }); + + it('round-trips a domain through the database', async () => { + const created = await service.create({ + title: 'Round Trip', + description: 'desc', + }); + const fetched = await service.getById(created.id!); + + expect(fetched).toBeInstanceOf(PositionDomain); + expect(fetched.id).toBe(created.id); + expect(fetched.title).toBe('Round Trip'); + expect(fetched.description).toBe('desc'); + }); +}); diff --git a/apps/api/test/integration/contexts/organisation/service/team.service.integration-spec.ts b/apps/api/test/integration/contexts/organisation/service/team.service.integration-spec.ts new file mode 100644 index 00000000..84208dc8 --- /dev/null +++ b/apps/api/test/integration/contexts/organisation/service/team.service.integration-spec.ts @@ -0,0 +1,288 @@ +import '../../../setup-env'; + +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { TeamService } from 'src/contexts/organisation/application/services/team.service'; +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createOrganisationTestModule, + resetOrganisationTables, +} from '../test-app-organisation'; + +describe('TeamService (integration)', () => { + let module: TestingModule; + let service: TeamService; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let alice: UserDomain; + let bob: UserDomain; + + beforeAll(async () => { + module = await createOrganisationTestModule(); + service = module.get(TeamService); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetOrganisationTables(prisma); + alice = await identityUsers.create({ + firstName: 'Alice', + lastName: 'Brown', + email: `alice.${Date.now()}@example.com`, + } as any); + bob = await identityUsers.create({ + firstName: 'Bob', + lastName: 'Green', + email: `bob.${Date.now()}@example.com`, + } as any); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('create', () => { + it('persists a team without a head', async () => { + const created = await service.create({ title: 'Platform' }); + + expect(created.id).toBeDefined(); + expect(created.title).toBe('Platform'); + expect(created.headId).toBeNull(); + + const fromDb = await prisma.team.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).not.toBeNull(); + }); + + it('verifies the head user exists when headId is supplied', async () => { + const created = await service.create({ + title: 'Platform', + headId: alice.id!, + }); + expect(created.headId).toBe(alice.id); + }); + + it('throws NotFoundException for a missing head user', async () => { + await expect( + service.create({ title: 'Platform', headId: 999999 }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('search', () => { + beforeEach(async () => { + await service.create({ title: 'Platform Engineering' }); + await service.create({ title: 'Product Engineering' }); + await service.create({ title: 'Design' }); + }); + + it('filters teams by title (case insensitive substring)', async () => { + const result = await service.search({ + title: 'engineering', + } as any); + + expect(result.map((t) => t.title).sort()).toEqual([ + 'Platform Engineering', + 'Product Engineering', + ]); + }); + + it('returns all teams without filter', async () => { + const all = await service.search({} as any); + expect(all).toHaveLength(3); + }); + }); + + describe('getById', () => { + it('returns the team when found', async () => { + const created = await service.create({ title: 'Platform' }); + + await expect(service.getById(created.id!)).resolves.toBeInstanceOf( + TeamDomain, + ); + }); + + it('throws NotFoundException when the team does not exist', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('forwards the patch and persists the changes', async () => { + const created = await service.create({ title: 'Platform' }); + + const updated = await service.update(created.id!, { + description: 'Updated', + headId: alice.id!, + }); + + expect(updated.description).toBe('Updated'); + expect(updated.headId).toBe(alice.id); + }); + + it('throws NotFoundException for a missing team', async () => { + await expect( + service.update(999999, { title: 'X' }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('throws NotFoundException for a missing head user', async () => { + const created = await service.create({ title: 'Platform' }); + + await expect( + service.update(created.id!, { headId: 999999 }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('delete', () => { + it('removes a team that has no members', async () => { + const created = await service.create({ title: 'Doomed' }); + + await service.delete(created.id!); + + const fromDb = await prisma.team.findUnique({ + where: { id: created.id! }, + }); + expect(fromDb).toBeNull(); + }); + + it('rejects deleting a team that still has members', async () => { + const created = await service.create({ title: 'Has members' }); + await service.addMember(created.id!, { + memberId: alice.id!, + isPrimary: true, + }); + + await expect(service.delete(created.id!)).rejects.toBeInstanceOf( + BadRequestException, + ); + + const stillThere = await prisma.team.findUnique({ + where: { id: created.id! }, + }); + expect(stillThere).not.toBeNull(); + }); + + it('throws NotFoundException when the team does not exist', async () => { + await expect(service.delete(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('addMember / listMembers / removeMember', () => { + it('adds a member without user data by default', async () => { + const team = await service.create({ title: 'Platform' }); + + const membership = await service.addMember(team.id!, { + memberId: alice.id!, + isPrimary: true, + }); + + expect(membership.id).toBeDefined(); + expect(membership.teamId).toBe(team.id); + expect(membership.memberId).toBe(alice.id); + expect(membership.isPrimary).toBe(true); + expect(membership.user).toBeUndefined(); + + const rows = await prisma.teamMembership.findMany({ + where: { teamId: team.id! }, + }); + expect(rows).toHaveLength(1); + }); + + it('attaches the user when withUser is set', async () => { + const team = await service.create({ title: 'Platform' }); + + const membership = await service.addMember( + team.id!, + { memberId: alice.id!, isPrimary: false }, + { withUser: true }, + ); + + expect(membership.user).toBeDefined(); + expect(membership.user!.id).toBe(alice.id); + }); + + it('rejects adding a missing user', async () => { + const team = await service.create({ title: 'Platform' }); + + await expect( + service.addMember(team.id!, { + memberId: 999999, + isPrimary: false, + }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('rejects adding a member to a missing team', async () => { + await expect( + service.addMember(999999, { + memberId: alice.id!, + isPrimary: false, + }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('listMembers returns memberships without users by default', async () => { + const team = await service.create({ title: 'Platform' }); + await service.addMember(team.id!, { + memberId: alice.id!, + isPrimary: true, + }); + await service.addMember(team.id!, { + memberId: bob.id!, + isPrimary: false, + }); + + const result = await service.listMembers(team.id!); + + expect(result).toHaveLength(2); + result.forEach((m) => expect(m.user).toBeUndefined()); + }); + + it('listMembers attaches users when withUsers is set', async () => { + const team = await service.create({ title: 'Platform' }); + await service.addMember(team.id!, { + memberId: alice.id!, + isPrimary: false, + }); + await service.addMember(team.id!, { + memberId: bob.id!, + isPrimary: false, + }); + + const result = await service.listMembers(team.id!, { + withUsers: true, + }); + + const ids = result + .map((m) => m.user?.id) + .filter((id): id is number => id !== undefined); + expect(ids.sort()).toEqual([alice.id!, bob.id!].sort()); + }); + + it('removeMember deletes the membership row', async () => { + const team = await service.create({ title: 'Platform' }); + await service.addMember(team.id!, { + memberId: alice.id!, + isPrimary: false, + }); + + await service.removeMember(team.id!, alice.id!); + + const rows = await prisma.teamMembership.findMany({ + where: { teamId: team.id! }, + }); + expect(rows).toEqual([]); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/organisation/test-app-organisation.ts b/apps/api/test/integration/contexts/organisation/test-app-organisation.ts new file mode 100644 index 00000000..034f0540 --- /dev/null +++ b/apps/api/test/integration/contexts/organisation/test-app-organisation.ts @@ -0,0 +1,114 @@ +import { ConfigModule } from '@nestjs/config'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { Test, TestingModule } from '@nestjs/testing'; +import appConfig from 'src/config/app'; +import databaseConfig from 'src/config/database'; +import { IDENTITY_ROLE_REPOSITORY } from 'src/contexts/identity/application/ports/role.repository.port'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { RoleRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/role.repository'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { POSITION_COMPETENCE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-competence-relation.repository.port'; +import { PositionCompetenceRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-competence-relation.repository'; +import { ORGANISATION_POSITION_HIERARCHY_REPOSITORY } from 'src/contexts/organisation/application/ports/position-hierarchy.repository.port'; +import { ORGANISATION_POSITION_REPOSITORY } from 'src/contexts/organisation/application/ports/position.repository.port'; +import { ORGANISATION_TEAM_REPOSITORY } from 'src/contexts/organisation/application/ports/team.repository.port'; +import { PositionHierarchyService } from 'src/contexts/organisation/application/services/position-hierarchy.service'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { TeamService } from 'src/contexts/organisation/application/services/team.service'; +import { PositionHierarchyRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position-hierarchy.repository'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { TeamRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/team.repository'; +import { DatabaseModule } from 'src/database/database.module'; +import { PrismaService } from 'src/database/prisma.service'; +import { ensureTestDatabaseExists } from '../../setup-env'; + +/** + * Bootstraps a Nest testing module for the organisation context wired + * to the real database. We assemble the providers manually instead of + * importing `OrganisationModule` because that module pulls in + * `AuthModule`, which would require the full HTTP / better-auth setup + * for service-level tests that never need it. + * + * Identity providers are included because `TeamService` depends on + * `IdentityUserService` to validate team head/member references against + * real users. The `library` position-competence relation repository is + * also included so `PositionService.delete()` can talk to its real + * table when no competences are linked. + */ +export async function createOrganisationTestModule(): Promise { + await ensureTestDatabaseExists(); + + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig], + ignoreEnvFile: true, + }), + EventEmitterModule.forRoot(), + DatabaseModule, + ], + providers: [ + // Organisation + PositionService, + PositionHierarchyService, + TeamService, + PositionRepository, + PositionHierarchyRepository, + TeamRepository, + { + provide: ORGANISATION_POSITION_REPOSITORY, + useExisting: PositionRepository, + }, + { + provide: ORGANISATION_POSITION_HIERARCHY_REPOSITORY, + useExisting: PositionHierarchyRepository, + }, + { + provide: ORGANISATION_TEAM_REPOSITORY, + useExisting: TeamRepository, + }, + // Identity (TeamService uses IdentityUserService) + IdentityUserService, + IdentityRoleService, + UserRepository, + RoleRepository, + { provide: IDENTITY_USER_REPOSITORY, useExisting: UserRepository }, + { provide: IDENTITY_ROLE_REPOSITORY, useExisting: RoleRepository }, + // Library cross-context dependency + PositionCompetenceRelationRepository, + { + provide: POSITION_COMPETENCE_RELATION_REPOSITORY, + useExisting: PositionCompetenceRelationRepository, + }, + ], + }).compile(); + + await module.init(); + return module; +} + +/** + * Truncates organisation-owned tables and the identity user tables + * between tests. We TRUNCATE identity users too because TeamService + * scenarios create them inline; `identity_roles` (reference data) is + * preserved. + */ +export async function resetOrganisationTables( + prisma: PrismaService, +): Promise { + await prisma.$executeRawUnsafe( + `TRUNCATE TABLE + "org_team_memberships", + "org_teams", + "org_position_hierarchies", + "library_position_competence_relations", + "library_competences", + "org_positions", + "identity_users_roles", + "identity_users" + RESTART IDENTITY CASCADE`, + ); +} diff --git a/apps/api/test/integration/contexts/reporting/listeners/cycle-stage.listener.integration-spec.ts b/apps/api/test/integration/contexts/reporting/listeners/cycle-stage.listener.integration-spec.ts new file mode 100644 index 00000000..e10e5b0c --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/listeners/cycle-stage.listener.integration-spec.ts @@ -0,0 +1,233 @@ +import '../../../setup-env'; + +import { + AnswerType, + CycleStage, + EntityType, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleStageProcessedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-processed.event'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { AnswerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { CycleStageListener } from 'src/contexts/reporting/application/listeners/cycle-stage.listener'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportAnalyticsRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-analytics.repository'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { StrategicReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('CycleStageListener (reporting, integration)', () => { + // The production listener schedules a setTimeout(..., 10000) that + // queries Prisma after the test (and the pool) has shut down. Fake + // timers prevent that timeout from running so Jest can exit cleanly. + // We exempt setImmediate/setInterval/nextTick so Prisma's own + // internals keep working. + beforeAll(() => { + jest.useFakeTimers({ + doNotFake: ['nextTick', 'setImmediate', 'setInterval'], + }); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + let module: TestingModule; + let listener: CycleStageListener; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let questionRepo: QuestionRepository; + let answerRepo: AnswerRepository; + let reportRepo: ReportRepository; + let reportAnalyticsRepo: ReportAnalyticsRepository; + let strategicRepo: StrategicReportRepository; + let competenceRepo: CompetenceRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let hrId: number; + let rateeId: number; + let positionId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + listener = module.get(CycleStageListener); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + questionRepo = module.get(QuestionRepository); + answerRepo = module.get(AnswerRepository); + reportRepo = module.get(ReportRepository); + reportAnalyticsRepo = module.get(ReportAnalyticsRepository); + strategicRepo = module.get(StrategicReportRepository); + competenceRepo = module.get(CompetenceRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function seedFinishedCycleWithReportAndCompetence(): Promise<{ + cycleId: number; + reviewId: number; + competenceId: number; + }> { + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId, + stage: CycleStage.FINISHED, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const competence = await competenceRepo.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const question = await questionRepo.create( + QuestionDomain.create({ + cycleId: cycle.id!, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: competence.id!, + }), + ); + // generateStrategicReportForCycle needs at least one Report + // tied to the cycle + at least one answer so allCompetences + // populates (otherwise insight rows reference competenceId=-1 + // and the FK fails). + const report = await reportRepo.create( + ReportDomain.create({ + reviewId: review.id!, + cycleId: cycle.id!, + respondentCount: 3, + respondentCategories: [ + RespondentCategory.TEAM, + RespondentCategory.OTHER, + ], + answerCount: 12, + turnoutPctOfTeam: 80, + turnoutPctOfOther: 50, + analytics: [], + }), + ); + await reportAnalyticsRepo.createMany(report.id!, [ + ReportAnalyticsDomain.create({ + reportId: report.id!, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: competence.id!, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4.5, + averageByTeam: 3.8, + averageByOther: 4.0, + }), + ]); + await answerRepo.create( + AnswerDomain.create({ + reviewId: review.id!, + questionId: question.id!, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }), + ); + return { + cycleId: cycle.id!, + reviewId: review.id!, + competenceId: competence.id!, + }; + } + + describe('handleCycleStageProcessed', () => { + it('does nothing when currentStage is not FINISHED', async () => { + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId, + stage: CycleStage.ACTIVE, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + + await listener.handleCycleStageProcessed( + new CycleStageProcessedEvent(cycle.id!, CycleStage.ACTIVE), + ); + + const cycleAfter = await cycleRepo.findById(cycle.id!); + expect(cycleAfter!.stage).toBe(CycleStage.ACTIVE); + const strategic = await strategicRepo.findByCycleId(cycle.id!); + expect(strategic).toBeNull(); + }); + + it('drives the cycle FINISHED → PREPARING_REPORT → PUBLISHED and generates a strategic report', async () => { + const { cycleId } = + await seedFinishedCycleWithReportAndCompetence(); + + await listener.handleCycleStageProcessed( + new CycleStageProcessedEvent(cycleId, CycleStage.FINISHED), + ); + + const cycleAfter = await cycleRepo.findById(cycleId); + expect(cycleAfter!.stage).toBe(CycleStage.PUBLISHED); + + const strategic = await strategicRepo.findByCycleId(cycleId); + expect(strategic).not.toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/listeners/review-stage.listener.integration-spec.ts b/apps/api/test/integration/contexts/reporting/listeners/review-stage.listener.integration-spec.ts new file mode 100644 index 00000000..4c4e5c78 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/listeners/review-stage.listener.integration-spec.ts @@ -0,0 +1,134 @@ +import '../../../setup-env'; + +import { ReviewStage } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { ReviewStageProcessedEvent } from 'src/contexts/feedback360/application/events/review-stage-processed.event'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReviewStageListener } from 'src/contexts/reporting/application/listeners/review-stage.listener'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReviewStageListener (reporting, integration)', () => { + let module: TestingModule; + let listener: ReviewStageListener; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let reportRepo: ReportRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let hrId: number; + let rateeId: number; + let positionId: number; + let cycleId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + listener = module.get(ReviewStageListener); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + reportRepo = module.get(ReportRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + + hrId = hr.id!; + rateeId = ratee.id!; + positionId = position.id!; + cycleId = cycle.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function createReview(stage: ReviewStage): Promise { + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId, + rateeFullName: 'Robert Smith', + rateePositionId: positionId, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId, + stage, + }), + ); + return review.id!; + } + + describe('handleReviewStageProcessed', () => { + it('does nothing when currentStage is not FINISHED', async () => { + const reviewId = await createReview(ReviewStage.IN_PROGRESS); + + await listener.handleReviewStageProcessed( + new ReviewStageProcessedEvent( + reviewId, + ReviewStage.IN_PROGRESS, + ), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.IN_PROGRESS); + + const report = await reportRepo.findByReviewId(reviewId); + expect(report).toBeNull(); + }); + + it('drives review FINISHED → PREPARING_REPORT → PROCESSING_BY_HR and generates an individual report', async () => { + const reviewId = await createReview(ReviewStage.FINISHED); + + await listener.handleReviewStageProcessed( + new ReviewStageProcessedEvent(reviewId, ReviewStage.FINISHED), + ); + + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.stage).toBe(ReviewStage.PROCESSING_BY_HR); + + const report = await reportRepo.findByReviewId(reviewId); + expect(report).not.toBeNull(); + expect(report!.reviewId).toBe(reviewId); + + // The review should also have its reportId set back to the + // newly-created report. + expect(reviewAfter!.reportId).toBe(report!.id); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/repository/report-analytics.repository.integration-spec.ts b/apps/api/test/integration/contexts/reporting/repository/report-analytics.repository.integration-spec.ts new file mode 100644 index 00000000..4cce2a00 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/repository/report-analytics.repository.integration-spec.ts @@ -0,0 +1,183 @@ +import '../../../setup-env'; + +import { + EntityType, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportAnalyticsRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-analytics.repository'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReportAnalyticsRepository (integration)', () => { + let module: TestingModule; + let repo: ReportAnalyticsRepository; + let reports: ReportRepository; + let reviews: ReviewRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let reportId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + repo = module.get(ReportAnalyticsRepository); + reports = module.get(ReportRepository); + reviews = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const report = await reports.create( + ReportDomain.create({ + reviewId: review.id!, + cycleId: cycle.id!, + respondentCount: 4, + respondentCategories: [ + RespondentCategory.TEAM, + RespondentCategory.OTHER, + ], + answerCount: 12, + analytics: [], + }), + ); + reportId = report.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildAnalytics( + overrides: Partial<{ + entityType: EntityType; + questionId: number | null; + competenceId: number | null; + }> = {}, + ): ReportAnalyticsDomain { + return ReportAnalyticsDomain.create({ + reportId, + entityType: overrides.entityType ?? EntityType.COMPETENCE, + questionId: overrides.questionId ?? null, + questionTitle: null, + competenceId: overrides.competenceId ?? null, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4.5, + averageByTeam: 3.8, + averageByOther: 4.0, + }); + } + + describe('createMany', () => { + it('persists multiple analytics rows tied to a report', async () => { + await repo.createMany(reportId, [ + buildAnalytics(), + buildAnalytics({ entityType: EntityType.QUESTION }), + ]); + + const rows = await prisma.reportAnalytics.findMany({ + where: { reportId }, + }); + expect(rows).toHaveLength(2); + }); + + it('is a no-op for an empty list', async () => { + await repo.createMany(reportId, []); + + const rows = await prisma.reportAnalytics.findMany({ + where: { reportId }, + }); + expect(rows).toEqual([]); + }); + }); + + describe('findByReportId', () => { + it('returns analytics in ascending id order', async () => { + await repo.createMany(reportId, [ + buildAnalytics({ entityType: EntityType.COMPETENCE }), + buildAnalytics({ entityType: EntityType.QUESTION }), + ]); + + const result = await repo.findByReportId(reportId); + + expect(result).toHaveLength(2); + expect(result.map((r) => r.entityType)).toEqual([ + EntityType.COMPETENCE, + EntityType.QUESTION, + ]); + }); + + it('returns an empty array when no analytics exist', async () => { + await expect(repo.findByReportId(reportId)).resolves.toEqual([]); + }); + }); + + describe('findById', () => { + it('returns the analytics row when found', async () => { + await repo.createMany(reportId, [buildAnalytics()]); + const stored = await prisma.reportAnalytics.findFirst({ + where: { reportId }, + }); + + await expect(repo.findById(stored!.id)).resolves.toBeInstanceOf( + ReportAnalyticsDomain, + ); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/repository/report-comment.repository.integration-spec.ts b/apps/api/test/integration/contexts/reporting/repository/report-comment.repository.integration-spec.ts new file mode 100644 index 00000000..1b11d407 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/repository/report-comment.repository.integration-spec.ts @@ -0,0 +1,186 @@ +import '../../../setup-env'; + +import { + AnswerType, + CommentSentiment, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReportCommentDomain } from 'src/contexts/reporting/domain/report-comment.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportCommentRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-comment.repository'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReportCommentRepository (integration)', () => { + let module: TestingModule; + let repo: ReportCommentRepository; + let reports: ReportRepository; + let reviews: ReviewRepository; + let cycles: CycleRepository; + let questions: QuestionRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let reportId: number; + let questionId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + repo = module.get(ReportCommentRepository); + reports = module.get(ReportRepository); + reviews = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + questions = module.get(QuestionRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const question = await questions.create( + QuestionDomain.create({ + cycleId: cycle.id!, + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + }), + ); + const report = await reports.create( + ReportDomain.create({ + reviewId: review.id!, + cycleId: cycle.id!, + respondentCount: 4, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 12, + analytics: [], + }), + ); + + reportId = report.id!; + questionId = question.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildComment( + overrides: Partial<{ + commentSentiment: CommentSentiment | null; + numberOfMentions: number; + comment: string; + }> = {}, + ): ReportCommentDomain { + return ReportCommentDomain.create({ + reportId, + questionId, + questionTitle: 'Q?', + comment: overrides.comment ?? 'Great teamwork', + respondentCategories: [ + RespondentCategory.TEAM, + RespondentCategory.OTHER, + ], + commentSentiment: + overrides.commentSentiment ?? CommentSentiment.POSITIVE, + numberOfMentions: overrides.numberOfMentions ?? 3, + }); + } + + describe('create', () => { + it('persists a comment row', async () => { + const created = await repo.create(buildComment()); + + expect(created.id).toBeDefined(); + expect(created.comment).toBe('Great teamwork'); + expect(created.commentSentiment).toBe(CommentSentiment.POSITIVE); + expect(created.numberOfMentions).toBe(3); + }); + + it('persists a comment with no sentiment', async () => { + const created = await repo.create( + buildComment({ commentSentiment: null }), + ); + + // Prisma column defaults to POSITIVE when sentiment is undefined + // in the input — the mapper drops null. So we expect the default. + expect(created.commentSentiment).toBe(CommentSentiment.POSITIVE); + }); + }); + + describe('findByReportId', () => { + it('returns comments in ascending id order', async () => { + await repo.create(buildComment({ comment: 'First' })); + await repo.create(buildComment({ comment: 'Second' })); + + const result = await repo.findByReportId(reportId); + + expect(result.map((c) => c.comment)).toEqual(['First', 'Second']); + }); + + it('returns an empty array when no comments exist', async () => { + await expect(repo.findByReportId(reportId)).resolves.toEqual([]); + }); + }); + + describe('findById', () => { + it('returns the comment when found', async () => { + const created = await repo.create(buildComment()); + + await expect(repo.findById(created.id!)).resolves.toBeInstanceOf( + ReportCommentDomain, + ); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/repository/report-insight.repository.integration-spec.ts b/apps/api/test/integration/contexts/reporting/repository/report-insight.repository.integration-spec.ts new file mode 100644 index 00000000..e38670e0 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/repository/report-insight.repository.integration-spec.ts @@ -0,0 +1,179 @@ +import '../../../setup-env'; + +import { + EntityType, + InsightType, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReportInsightDomain } from 'src/contexts/reporting/domain/report-insight.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportInsightRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-insight.repository'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReportInsightRepository (integration)', () => { + let module: TestingModule; + let repo: ReportInsightRepository; + let reports: ReportRepository; + let reviews: ReviewRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let reportId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + repo = module.get(ReportInsightRepository); + reports = module.get(ReportRepository); + reviews = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const report = await reports.create( + ReportDomain.create({ + reviewId: review.id!, + cycleId: cycle.id!, + respondentCount: 4, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 12, + analytics: [], + }), + ); + reportId = report.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildInsight( + overrides: Partial<{ + insightType: InsightType; + averageScore: number; + }> = {}, + ): ReportInsightDomain { + return ReportInsightDomain.create({ + reportId, + insightType: overrides.insightType ?? InsightType.HIGHEST_RATING, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: null, + competenceTitle: 'Teamwork', + averageScore: overrides.averageScore ?? 4.5, + }); + } + + describe('createMany', () => { + it('persists multiple insight rows tied to a report', async () => { + await repo.createMany(reportId, [ + buildInsight(), + buildInsight({ insightType: InsightType.LOWEST_RATING }), + ]); + + const rows = await prisma.reportInsights.findMany({ + where: { reportId }, + }); + expect(rows).toHaveLength(2); + }); + + it('is a no-op for an empty list', async () => { + await repo.createMany(reportId, []); + + const rows = await prisma.reportInsights.findMany({ + where: { reportId }, + }); + expect(rows).toEqual([]); + }); + }); + + describe('findByReportId', () => { + it('returns insights in ascending id order', async () => { + await repo.createMany(reportId, [ + buildInsight({ insightType: InsightType.HIGHEST_RATING }), + buildInsight({ insightType: InsightType.LOWEST_RATING }), + ]); + + const result = await repo.findByReportId(reportId); + + expect(result).toHaveLength(2); + expect(result.map((r) => r.insightType)).toEqual([ + InsightType.HIGHEST_RATING, + InsightType.LOWEST_RATING, + ]); + }); + + it('returns an empty array when no insights exist', async () => { + await expect(repo.findByReportId(reportId)).resolves.toEqual([]); + }); + }); + + describe('findById', () => { + it('returns the insight when found', async () => { + await repo.createMany(reportId, [buildInsight()]); + const stored = await prisma.reportInsights.findFirst({ + where: { reportId }, + }); + + await expect(repo.findById(stored!.id)).resolves.toBeInstanceOf( + ReportInsightDomain, + ); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/repository/report.repository.integration-spec.ts b/apps/api/test/integration/contexts/reporting/repository/report.repository.integration-spec.ts new file mode 100644 index 00000000..ca19df5f --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/repository/report.repository.integration-spec.ts @@ -0,0 +1,219 @@ +import '../../../setup-env'; + +import { + ReportSortField, + RespondentCategory, + ReviewStage, + SortDirection, +} from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReportRepository (integration)', () => { + let module: TestingModule; + let repo: ReportRepository; + let reviews: ReviewRepository; + let cycles: CycleRepository; + let positions: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createReportingTestModule(); + repo = module.get(ReportRepository); + reviews = module.get(ReviewRepository); + cycles = module.get(CycleRepository); + positions = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + async function seedReview(): Promise<{ + reviewId: number; + cycleId: number; + }> { + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positions.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviews.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + return { reviewId: review.id!, cycleId: cycle.id! }; + } + + function buildReport( + reviewId: number, + cycleId: number, + overrides: Partial<{ + respondentCount: number; + answerCount: number; + }> = {}, + ): ReportDomain { + return ReportDomain.create({ + reviewId, + cycleId, + respondentCount: overrides.respondentCount ?? 4, + respondentCategories: [ + RespondentCategory.SELF_ASSESSMENT, + RespondentCategory.TEAM, + RespondentCategory.OTHER, + ], + answerCount: overrides.answerCount ?? 24, + turnoutPctOfTeam: 0.75, + turnoutPctOfOther: 0.5, + analytics: [], + }); + } + + describe('create / findById', () => { + it('persists a report with relations defaulting to empty arrays', async () => { + const { reviewId, cycleId } = await seedReview(); + + const created = await repo.create(buildReport(reviewId, cycleId)); + + expect(created.id).toBeDefined(); + expect(created.reviewId).toBe(reviewId); + expect(created.cycleId).toBe(cycleId); + expect(created.analytics).toEqual([]); + expect(created.comments).toEqual([]); + expect(created.insights).toEqual([]); + }); + + it('findById returns the report with included relations', async () => { + const { reviewId, cycleId } = await seedReview(); + const persisted = await repo.create(buildReport(reviewId, cycleId)); + + const fetched = await repo.findById(persisted.id!); + + expect(fetched).toBeInstanceOf(ReportDomain); + expect(fetched!.id).toBe(persisted.id); + }); + + it('findById returns null when missing', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('findByReviewId', () => { + it('returns the report attached to a review', async () => { + const { reviewId, cycleId } = await seedReview(); + await repo.create(buildReport(reviewId, cycleId)); + + const fetched = await repo.findByReviewId(reviewId); + + expect(fetched).toBeInstanceOf(ReportDomain); + expect(fetched!.reviewId).toBe(reviewId); + }); + + it('returns null when no report exists for a review', async () => { + const { reviewId } = await seedReview(); + await expect(repo.findByReviewId(reviewId)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + it('returns all reports when no filter is supplied', async () => { + const a = await seedReview(); + const b = await seedReview(); + await repo.create(buildReport(a.reviewId, a.cycleId)); + await repo.create(buildReport(b.reviewId, b.cycleId)); + + const all = await repo.search({} as any); + expect(all).toHaveLength(2); + }); + + it('filters by cycleId', async () => { + const a = await seedReview(); + const b = await seedReview(); + await repo.create(buildReport(a.reviewId, a.cycleId)); + await repo.create(buildReport(b.reviewId, b.cycleId)); + + const result = await repo.search({ + cycleId: a.cycleId, + } as any); + + expect(result).toHaveLength(1); + expect(result[0].cycleId).toBe(a.cycleId); + }); + + it('filters by respondentCount', async () => { + const a = await seedReview(); + const b = await seedReview(); + await repo.create( + buildReport(a.reviewId, a.cycleId, { respondentCount: 3 }), + ); + await repo.create( + buildReport(b.reviewId, b.cycleId, { respondentCount: 7 }), + ); + + const result = await repo.search({ + respondentCount: 7, + } as any); + + expect(result).toHaveLength(1); + expect(result[0].respondentCount).toBe(7); + }); + + it('honours descending sort direction on createdAt', async () => { + const a = await seedReview(); + const b = await seedReview(); + await repo.create(buildReport(a.reviewId, a.cycleId)); + await repo.create(buildReport(b.reviewId, b.cycleId)); + + const result = await repo.search({ + sortBy: ReportSortField.CREATED_AT, + sortDirection: SortDirection.DESC, + } as any); + + expect(result).toHaveLength(2); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/repository/strategic-report-analytics.repository.integration-spec.ts b/apps/api/test/integration/contexts/reporting/repository/strategic-report-analytics.repository.integration-spec.ts new file mode 100644 index 00000000..e8991412 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/repository/strategic-report-analytics.repository.integration-spec.ts @@ -0,0 +1,163 @@ +import '../../../setup-env'; + +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { StrategicReportAnalyticsDomain } from 'src/contexts/reporting/domain/strategic-report-analytics.domain'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { StrategicReportAnalyticsRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository'; +import { StrategicReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('StrategicReportAnalyticsRepository (integration)', () => { + let module: TestingModule; + let repo: StrategicReportAnalyticsRepository; + let strategicReports: StrategicReportRepository; + let cycles: CycleRepository; + let competences: CompetenceRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let strategicReportId: number; + let competenceId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + repo = module.get(StrategicReportAnalyticsRepository); + strategicReports = module.get(StrategicReportRepository); + cycles = module.get(CycleRepository); + competences = module.get(CompetenceRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const strategicReport = await strategicReports.create( + StrategicReportDomain.create({ + cycleId: cycle.id!, + cycleTitle: 'Cycle', + rateeCount: 5, + rateeIds: [], + respondentCount: 12, + respondentIds: [], + answerCount: 100, + reviewerCount: 5, + reviewerIds: [], + teamCount: 2, + teamIds: [], + positionCount: 3, + positionIds: [], + competenceCount: 1, + competenceIds: [competence.id!], + questionCount: 0, + questionIds: [], + }), + ); + + strategicReportId = strategicReport.id!; + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildAnalytics( + overrides: Partial<{ averageByTeam: number }> = {}, + ): StrategicReportAnalyticsDomain { + return StrategicReportAnalyticsDomain.create({ + strategicReportId, + competenceId, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4.5, + averageByTeam: overrides.averageByTeam ?? 3.8, + averageByOther: 4.1, + }); + } + + describe('createMany', () => { + it('persists multiple analytics rows', async () => { + await repo.createMany(strategicReportId, [ + buildAnalytics(), + buildAnalytics({ averageByTeam: 4.0 }), + ]); + + const rows = await prisma.strategicReportAnalytics.findMany({ + where: { strategicReportId }, + }); + expect(rows).toHaveLength(2); + }); + + it('is a no-op for an empty list', async () => { + await repo.createMany(strategicReportId, []); + + const rows = await prisma.strategicReportAnalytics.findMany({ + where: { strategicReportId }, + }); + expect(rows).toEqual([]); + }); + }); + + describe('findByStrategicReportId', () => { + it('returns analytics in ascending id order', async () => { + await repo.createMany(strategicReportId, [ + buildAnalytics(), + buildAnalytics({ averageByTeam: 4.0 }), + ]); + + const result = + await repo.findByStrategicReportId(strategicReportId); + + expect(result).toHaveLength(2); + expect(result[0].averageByTeam!.toNumber()).toBe(3.8); + expect(result[1].averageByTeam!.toNumber()).toBe(4.0); + }); + + it('returns an empty array when no analytics exist', async () => { + await expect( + repo.findByStrategicReportId(strategicReportId), + ).resolves.toEqual([]); + }); + }); + + describe('findById', () => { + it('returns the analytics row when found', async () => { + await repo.createMany(strategicReportId, [buildAnalytics()]); + const stored = await prisma.strategicReportAnalytics.findFirst({ + where: { strategicReportId }, + }); + + await expect(repo.findById(stored!.id)).resolves.toBeInstanceOf( + StrategicReportAnalyticsDomain, + ); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/repository/strategic-report-insight.repository.integration-spec.ts b/apps/api/test/integration/contexts/reporting/repository/strategic-report-insight.repository.integration-spec.ts new file mode 100644 index 00000000..7f450aa8 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/repository/strategic-report-insight.repository.integration-spec.ts @@ -0,0 +1,167 @@ +import '../../../setup-env'; + +import { InsightType } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { StrategicReportInsightDomain } from 'src/contexts/reporting/domain/startegic-report-insight.domain'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { StrategicReportInsightRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository'; +import { StrategicReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('StrategicReportInsightRepository (integration)', () => { + let module: TestingModule; + let repo: StrategicReportInsightRepository; + let strategicReports: StrategicReportRepository; + let cycles: CycleRepository; + let competences: CompetenceRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let strategicReportId: number; + let competenceId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + repo = module.get(StrategicReportInsightRepository); + strategicReports = module.get(StrategicReportRepository); + cycles = module.get(CycleRepository); + competences = module.get(CompetenceRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}@example.com`, + } as any); + const cycle = await cycles.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const competence = await competences.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const strategicReport = await strategicReports.create( + StrategicReportDomain.create({ + cycleId: cycle.id!, + cycleTitle: 'Cycle', + rateeCount: 5, + rateeIds: [], + respondentCount: 12, + respondentIds: [], + answerCount: 100, + reviewerCount: 5, + reviewerIds: [], + teamCount: 2, + teamIds: [], + positionCount: 3, + positionIds: [], + competenceCount: 1, + competenceIds: [competence.id!], + questionCount: 0, + questionIds: [], + }), + ); + + strategicReportId = strategicReport.id!; + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function buildInsight( + overrides: Partial<{ + insightType: InsightType; + averageScore: number; + }> = {}, + ): StrategicReportInsightDomain { + return StrategicReportInsightDomain.create({ + strategicReportId, + insightType: overrides.insightType ?? InsightType.HIGHEST_RATING, + competenceId, + competenceTitle: 'Teamwork', + averageScore: overrides.averageScore ?? 4.7, + }); + } + + describe('createMany', () => { + it('persists multiple insight rows', async () => { + await repo.createMany(strategicReportId, [ + buildInsight(), + buildInsight({ insightType: InsightType.LOWEST_RATING }), + ]); + + const rows = await prisma.strategicReportInsights.findMany({ + where: { strategicReportId }, + }); + expect(rows).toHaveLength(2); + }); + + it('is a no-op for an empty list', async () => { + await repo.createMany(strategicReportId, []); + + const rows = await prisma.strategicReportInsights.findMany({ + where: { strategicReportId }, + }); + expect(rows).toEqual([]); + }); + }); + + describe('findByStrategicReportId', () => { + it('returns insights in ascending id order', async () => { + await repo.createMany(strategicReportId, [ + buildInsight({ insightType: InsightType.HIGHEST_RATING }), + buildInsight({ insightType: InsightType.LOWEST_RATING }), + ]); + + const result = + await repo.findByStrategicReportId(strategicReportId); + + expect(result.map((r) => r.insightType)).toEqual([ + InsightType.HIGHEST_RATING, + InsightType.LOWEST_RATING, + ]); + }); + + it('returns an empty array when no insights exist', async () => { + await expect( + repo.findByStrategicReportId(strategicReportId), + ).resolves.toEqual([]); + }); + }); + + describe('findById', () => { + it('returns the insight when found', async () => { + await repo.createMany(strategicReportId, [buildInsight()]); + const stored = await prisma.strategicReportInsights.findFirst({ + where: { strategicReportId }, + }); + + await expect(repo.findById(stored!.id)).resolves.toBeInstanceOf( + StrategicReportInsightDomain, + ); + }); + + it('returns null for a missing id', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/repository/strategic-report.repository.integration-spec.ts b/apps/api/test/integration/contexts/reporting/repository/strategic-report.repository.integration-spec.ts new file mode 100644 index 00000000..eb9414b7 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/repository/strategic-report.repository.integration-spec.ts @@ -0,0 +1,188 @@ +import '../../../setup-env'; + +import { SortDirection, StrategicReportSortField } from '@intra/shared-kernel'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { StrategicReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('StrategicReportRepository (integration)', () => { + let module: TestingModule; + let repo: StrategicReportRepository; + let cycles: CycleRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + beforeAll(async () => { + module = await createReportingTestModule(); + repo = module.get(StrategicReportRepository); + cycles = module.get(CycleRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + }); + + afterAll(async () => { + await module.close(); + }); + + async function seedCycle(title: string): Promise { + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const cycle = await cycles.create( + CycleDomain.create({ + title, + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + return cycle.id!; + } + + function buildReport( + cycleId: number, + overrides: Partial<{ + cycleTitle: string; + rateeCount: number; + answerCount: number; + rateeIds: number[]; + teamIds: number[]; + }> = {}, + ): StrategicReportDomain { + return StrategicReportDomain.create({ + cycleId, + cycleTitle: overrides.cycleTitle ?? 'Q1 2026', + rateeCount: overrides.rateeCount ?? 5, + rateeIds: overrides.rateeIds ?? [1, 2, 3, 4, 5], + respondentCount: 12, + respondentIds: [10, 11, 12], + answerCount: overrides.answerCount ?? 120, + reviewerCount: 5, + reviewerIds: [], + teamCount: 2, + teamIds: overrides.teamIds ?? [100, 200], + positionCount: 3, + positionIds: [300, 301, 302], + competenceCount: 4, + competenceIds: [400, 401, 402, 403], + questionCount: 8, + questionIds: [500, 501, 502, 503, 504, 505, 506, 507], + turnoutAvgPctOfRatees: 0.85, + }); + } + + describe('create / findById', () => { + it('persists a strategic report with empty relations', async () => { + const cycleId = await seedCycle('Cycle'); + + const created = await repo.create(buildReport(cycleId)); + + expect(created.id).toBeDefined(); + expect(created.cycleId).toBe(cycleId); + expect(created.analytics).toEqual([]); + expect(created.insights).toEqual([]); + }); + + it('findById returns the report with included relations', async () => { + const cycleId = await seedCycle('Cycle'); + const created = await repo.create(buildReport(cycleId)); + + const fetched = await repo.findById(created.id!); + + expect(fetched).toBeInstanceOf(StrategicReportDomain); + expect(fetched!.cycleId).toBe(cycleId); + }); + + it('findById returns null when missing', async () => { + await expect(repo.findById(999999)).resolves.toBeNull(); + }); + }); + + describe('findByCycleId', () => { + it('returns the most recently created report for a cycle', async () => { + const cycleId = await seedCycle('Cycle'); + const first = await repo.create( + buildReport(cycleId, { cycleTitle: 'first' }), + ); + const second = await repo.create( + buildReport(cycleId, { cycleTitle: 'second' }), + ); + + const fetched = await repo.findByCycleId(cycleId); + + expect(fetched).toBeInstanceOf(StrategicReportDomain); + // Ordered by createdAt desc; the second.id should come back. + const candidateIds = [first.id, second.id]; + expect(candidateIds).toContain(fetched!.id); + }); + + it('returns null when no strategic report exists for a cycle', async () => { + const cycleId = await seedCycle('Cycle'); + + await expect(repo.findByCycleId(cycleId)).resolves.toBeNull(); + }); + }); + + describe('search', () => { + it('returns all reports when no filter is supplied', async () => { + const a = await seedCycle('Cycle A'); + const b = await seedCycle('Cycle B'); + await repo.create(buildReport(a)); + await repo.create(buildReport(b)); + + const all = await repo.search({} as any); + expect(all).toHaveLength(2); + }); + + it('filters by cycleId', async () => { + const a = await seedCycle('Cycle A'); + const b = await seedCycle('Cycle B'); + await repo.create(buildReport(a)); + await repo.create(buildReport(b)); + + const result = await repo.search({ cycleId: a } as any); + expect(result).toHaveLength(1); + expect(result[0].cycleId).toBe(a); + }); + + it('filters by rateeIds (hasSome)', async () => { + const a = await seedCycle('Cycle A'); + const b = await seedCycle('Cycle B'); + await repo.create(buildReport(a, { rateeIds: [1, 2] })); + await repo.create(buildReport(b, { rateeIds: [3, 4] })); + + const result = await repo.search({ rateeIds: [3] } as any); + + expect(result).toHaveLength(1); + expect(result[0].rateeIds).toEqual([3, 4]); + }); + + it('honours descending sort on createdAt', async () => { + const a = await seedCycle('Cycle A'); + const b = await seedCycle('Cycle B'); + await repo.create(buildReport(a)); + await repo.create(buildReport(b)); + + const result = await repo.search({ + sortBy: StrategicReportSortField.CREATED_AT, + sortDirection: SortDirection.DESC, + } as any); + + expect(result).toHaveLength(2); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/service/report-analytics.service.integration-spec.ts b/apps/api/test/integration/contexts/reporting/service/report-analytics.service.integration-spec.ts new file mode 100644 index 00000000..6f56a033 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/service/report-analytics.service.integration-spec.ts @@ -0,0 +1,198 @@ +import '../../../setup-env'; + +import { + EntityType, + IdentityRole, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReportAnalyticsService } from 'src/contexts/reporting/application/services/report-analytics.service'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportAnalyticsRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-analytics.repository'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReportAnalyticsService (integration)', () => { + let module: TestingModule; + let service: ReportAnalyticsService; + let analyticsRepo: ReportAnalyticsRepository; + let reportRepo: ReportRepository; + let reviewRepo: ReviewRepository; + let cycleRepo: CycleRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let hrId: number; + let reportId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + service = module.get(ReportAnalyticsService); + analyticsRepo = module.get(ReportAnalyticsRepository); + reportRepo = module.get(ReportRepository); + reviewRepo = module.get(ReviewRepository); + cycleRepo = module.get(CycleRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const report = await reportRepo.create( + ReportDomain.create({ + reviewId: review.id!, + cycleId: cycle.id!, + respondentCount: 3, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 6, + analytics: [], + }), + ); + + hrId = hr.id!; + reportId = report.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function hrActor(): UserDomain { + return UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + } + + async function seedAnalytics() { + await analyticsRepo.createMany(reportId, [ + ReportAnalyticsDomain.create({ + reportId, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: null, + competenceTitle: 'Teamwork', + averageByTeam: 3.7, + }), + ]); + } + + describe('getByReportId', () => { + it('returns analytics tied to the report when access is allowed', async () => { + await seedAnalytics(); + + const result = await service.getByReportId(reportId, hrActor()); + + expect(result).toHaveLength(1); + expect(result[0].competenceTitle).toBe('Teamwork'); + }); + + it('returns an empty array when no analytics rows exist (access still checked)', async () => { + // findByReportId returns [] which is truthy; the explicit null + // check in the service therefore never fires. + await expect( + service.getByReportId(reportId, hrActor()), + ).resolves.toEqual([]); + }); + + it('delegates access check to ReportingService (NotFound when report missing)', async () => { + await expect( + service.getByReportId(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('delegates access check to ReportingService (Forbidden for unrelated actor)', async () => { + const otherId = ( + await identityUsers.create({ + firstName: 'X', + lastName: 'Y', + email: `x.${Date.now()}.${Math.random()}@example.com`, + } as any) + ).id!; + const stranger = UserDomain.create({ + id: otherId, + firstName: 'X', + lastName: 'Y', + email: 'x@example.com', + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.getByReportId(reportId, stranger), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('getById', () => { + it('returns the analytics row when found and the actor has access', async () => { + await seedAnalytics(); + const stored = await prisma.reportAnalytics.findFirst({ + where: { reportId }, + }); + + const result = await service.getById(stored!.id, hrActor()); + + expect(result.id).toBe(stored!.id); + }); + + it('throws NotFoundException when analytics id is missing', async () => { + await expect( + service.getById(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/service/report-comment.service.integration-spec.ts b/apps/api/test/integration/contexts/reporting/service/report-comment.service.integration-spec.ts new file mode 100644 index 00000000..6b6dbb2a --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/service/report-comment.service.integration-spec.ts @@ -0,0 +1,192 @@ +import '../../../setup-env'; + +import { + AnswerType, + CommentSentiment, + IdentityRole, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReportCommentService } from 'src/contexts/reporting/application/services/report-comment.service'; +import { ReportCommentDomain } from 'src/contexts/reporting/domain/report-comment.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReportCommentService (integration)', () => { + let module: TestingModule; + let service: ReportCommentService; + let reportRepo: ReportRepository; + let reviewRepo: ReviewRepository; + let cycleRepo: CycleRepository; + let questionRepo: QuestionRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let hrId: number; + let reportId: number; + let questionId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + service = module.get(ReportCommentService); + reportRepo = module.get(ReportRepository); + reviewRepo = module.get(ReviewRepository); + cycleRepo = module.get(CycleRepository); + questionRepo = module.get(QuestionRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const question = await questionRepo.create( + QuestionDomain.create({ + cycleId: cycle.id!, + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + }), + ); + const report = await reportRepo.create( + ReportDomain.create({ + reviewId: review.id!, + cycleId: cycle.id!, + respondentCount: 3, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 6, + analytics: [], + }), + ); + + hrId = hr.id!; + reportId = report.id!; + questionId = question.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function hrActor(): UserDomain { + return UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + } + + function buildComment( + overrides: Partial<{ comment: string }> = {}, + ): ReportCommentDomain { + return ReportCommentDomain.create({ + reportId, + questionId, + questionTitle: 'Q?', + comment: overrides.comment ?? 'Great teamwork', + respondentCategories: [RespondentCategory.TEAM], + commentSentiment: CommentSentiment.POSITIVE, + numberOfMentions: 1, + }); + } + + describe('create', () => { + it('persists a comment via the repository', async () => { + const created = await service.create(buildComment()); + + expect(created.id).toBeDefined(); + expect(created.comment).toBe('Great teamwork'); + }); + }); + + describe('getByReportId', () => { + it('returns comments tied to a report for an HR actor', async () => { + await service.create(buildComment()); + await service.create(buildComment({ comment: 'Second' })); + + const result = await service.getByReportId(reportId, hrActor()); + + expect(result).toHaveLength(2); + }); + + it('throws NotFoundException when no comments exist for the report', async () => { + await expect( + service.getByReportId(reportId, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('delegates report-access check (throws when report is missing)', async () => { + await expect( + service.getByReportId(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('getById', () => { + it('returns the comment when found', async () => { + const created = await service.create(buildComment()); + + const result = await service.getById(created.id!); + + expect(result.id).toBe(created.id); + }); + + it('throws NotFoundException when missing', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/service/report-insight.service.integration-spec.ts b/apps/api/test/integration/contexts/reporting/service/report-insight.service.integration-spec.ts new file mode 100644 index 00000000..e4393478 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/service/report-insight.service.integration-spec.ts @@ -0,0 +1,197 @@ +import '../../../setup-env'; + +import { + EntityType, + IdentityRole, + InsightType, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReportInsightService } from 'src/contexts/reporting/application/services/report-insight.service'; +import { ReportInsightDomain } from 'src/contexts/reporting/domain/report-insight.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportInsightRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-insight.repository'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReportInsightService (integration)', () => { + let module: TestingModule; + let service: ReportInsightService; + let insightsRepo: ReportInsightRepository; + let reportRepo: ReportRepository; + let reviewRepo: ReviewRepository; + let cycleRepo: CycleRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let hrId: number; + let reportId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + service = module.get(ReportInsightService); + insightsRepo = module.get(ReportInsightRepository); + reportRepo = module.get(ReportRepository); + reviewRepo = module.get(ReviewRepository); + cycleRepo = module.get(CycleRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const report = await reportRepo.create( + ReportDomain.create({ + reviewId: review.id!, + cycleId: cycle.id!, + respondentCount: 3, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 6, + analytics: [], + }), + ); + + hrId = hr.id!; + reportId = report.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function hrActor(): UserDomain { + return UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + } + + async function seedInsights() { + await insightsRepo.createMany(reportId, [ + ReportInsightDomain.create({ + reportId, + insightType: InsightType.HIGHEST_RATING, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: null, + competenceTitle: 'Teamwork', + averageScore: 4.6, + }), + ]); + } + + describe('getByReportId', () => { + it('returns insights tied to the report for an HR actor', async () => { + await seedInsights(); + + const result = await service.getByReportId(reportId, hrActor()); + + expect(result).toHaveLength(1); + expect(result[0].insightType).toBe(InsightType.HIGHEST_RATING); + }); + + it('returns an empty array when no insights exist (access still checked)', async () => { + await expect( + service.getByReportId(reportId, hrActor()), + ).resolves.toEqual([]); + }); + + it('throws NotFoundException when report is missing', async () => { + await expect( + service.getByReportId(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('throws ForbiddenException for an unrelated actor', async () => { + const otherId = ( + await identityUsers.create({ + firstName: 'X', + lastName: 'Y', + email: `x.${Date.now()}.${Math.random()}@example.com`, + } as any) + ).id!; + const stranger = UserDomain.create({ + id: otherId, + firstName: 'X', + lastName: 'Y', + email: 'x@example.com', + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.getByReportId(reportId, stranger), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('getById', () => { + it('returns the insight when found and the actor has access', async () => { + await seedInsights(); + const stored = await prisma.reportInsights.findFirst({ + where: { reportId }, + }); + + const result = await service.getById(stored!.id, hrActor()); + + expect(result.id).toBe(stored!.id); + }); + + it('throws NotFoundException when insight id is missing', async () => { + await expect( + service.getById(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/service/reports.service.integration-spec.ts b/apps/api/test/integration/contexts/reporting/service/reports.service.integration-spec.ts new file mode 100644 index 00000000..8600b074 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/service/reports.service.integration-spec.ts @@ -0,0 +1,381 @@ +import '../../../setup-env'; + +import { + AnswerType, + IdentityRole, + RespondentCategory, + ResponseStatus, + ReviewStage, +} from '@intra/shared-kernel'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { AnswerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { RespondentRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository'; +import { ReviewQuestionRelationRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review-question-relation.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { ReportingService } from 'src/contexts/reporting/application/services/reports.service'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('ReportingService (integration)', () => { + let module: TestingModule; + let service: ReportingService; + let reportRepo: ReportRepository; + let reviewRepo: ReviewRepository; + let cycleRepo: CycleRepository; + let questionRepo: QuestionRepository; + let relationRepo: ReviewQuestionRelationRepository; + let answerRepo: AnswerRepository; + let respondentRepo: RespondentRepository; + let competenceRepo: CompetenceRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let hrUser: UserDomain; + let rateeUser: UserDomain; + + beforeAll(async () => { + module = await createReportingTestModule(); + service = module.get(ReportingService); + reportRepo = module.get(ReportRepository); + reviewRepo = module.get(ReviewRepository); + cycleRepo = module.get(CycleRepository); + questionRepo = module.get(QuestionRepository); + relationRepo = module.get(ReviewQuestionRelationRepository); + answerRepo = module.get(AnswerRepository); + respondentRepo = module.get(RespondentRepository); + competenceRepo = module.get(CompetenceRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + hrUser = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + rateeUser = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + }); + + afterAll(async () => { + await module.close(); + }); + + async function seedReview(): Promise<{ + reviewId: number; + cycleId: number; + }> { + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hrUser.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId: rateeUser.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hrUser.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + return { reviewId: review.id!, cycleId: cycle.id! }; + } + + async function seedReport( + reviewId: number, + cycleId: number, + ): Promise { + const report = await reportRepo.create( + ReportDomain.create({ + reviewId, + cycleId, + respondentCount: 3, + respondentCategories: [ + RespondentCategory.TEAM, + RespondentCategory.OTHER, + ], + answerCount: 12, + analytics: [], + }), + ); + return report.id!; + } + + function hrActor(): UserDomain { + return UserDomain.create({ + id: hrUser.id, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + } + + function nonPrivilegedActor(id: number): UserDomain { + return UserDomain.create({ + id, + firstName: 'Other', + lastName: 'Person', + email: 'other@example.com', + roles: [IdentityRole.EMPLOYEE], + }); + } + + describe('search / checkAccessToAllReports', () => { + it('returns persisted reports for an HR actor', async () => { + const { reviewId, cycleId } = await seedReview(); + await seedReport(reviewId, cycleId); + + const result = await service.search({} as any, hrActor()); + + expect(result).toHaveLength(1); + }); + + it('returns results when no actor is passed (system call)', async () => { + const { reviewId, cycleId } = await seedReview(); + await seedReport(reviewId, cycleId); + + const result = await service.search({} as any); + + expect(result).toHaveLength(1); + }); + + it('throws ForbiddenException for a non-HR / non-Admin actor', async () => { + const otherId = ( + await identityUsers.create({ + firstName: 'Other', + lastName: 'P', + email: `o.${Date.now()}.${Math.random()}@example.com`, + } as any) + ).id!; + + await expect( + service.search({} as any, nonPrivilegedActor(otherId)), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('getById / checkAccessToReport', () => { + it('returns the report when found and accessor is HR', async () => { + const { reviewId, cycleId } = await seedReview(); + const reportId = await seedReport(reviewId, cycleId); + + const result = await service.getById(reportId, hrActor()); + + expect(result.id).toBe(reportId); + }); + + it('returns the report when accessor is the ratee', async () => { + const { reviewId, cycleId } = await seedReview(); + const reportId = await seedReport(reviewId, cycleId); + const rateeActor = UserDomain.create({ + id: rateeUser.id, + firstName: 'R', + lastName: 'S', + email: 'ratee@example.com', + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.getById(reportId, rateeActor), + ).resolves.toBeDefined(); + }); + + it('throws ForbiddenException when accessor is unrelated to the review', async () => { + const { reviewId, cycleId } = await seedReview(); + const reportId = await seedReport(reviewId, cycleId); + const strangerId = ( + await identityUsers.create({ + firstName: 'X', + lastName: 'Y', + email: `x.${Date.now()}.${Math.random()}@example.com`, + } as any) + ).id!; + + await expect( + service.getById(reportId, nonPrivilegedActor(strangerId)), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + + it('throws NotFoundException for a missing id', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('getByReviewId', () => { + it('returns the report tied to a review', async () => { + const { reviewId, cycleId } = await seedReview(); + await seedReport(reviewId, cycleId); + + const result = await service.getByReviewId(reviewId, hrActor()); + + expect(result.reviewId).toBe(reviewId); + }); + + it('throws NotFoundException when no report exists for a review', async () => { + const { reviewId } = await seedReview(); + + await expect( + service.getByReviewId(reviewId, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('generateReportForReview', () => { + async function seedQuestionAndRelation( + reviewId: number, + cycleId: number, + ): Promise<{ questionId: number; competenceId: number }> { + const competence = await competenceRepo.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const question = await questionRepo.create( + QuestionDomain.create({ + cycleId, + title: 'Rate teamwork', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: competence.id!, + }), + ); + await relationRepo.link( + ReviewQuestionRelationDomain.create({ + reviewId, + questionId: question.id!, + questionTitle: 'Rate teamwork', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: competence.id!, + competenceTitle: 'Teamwork', + }), + ); + return { questionId: question.id!, competenceId: competence.id! }; + } + + async function seedRespondentAndAnswer( + reviewId: number, + questionId: number, + positionId: number, + category: RespondentCategory, + answerValue: number, + ) { + const user = await identityUsers.create({ + firstName: `R${Date.now()}${Math.random()}`, + lastName: 'Person', + email: `r.${Date.now()}.${Math.random()}@example.com`, + } as any); + await respondentRepo.create( + RespondentDomain.create({ + reviewId, + respondentId: user.id!, + fullName: user.fullName, + category, + responseStatus: ResponseStatus.COMPLETED, + positionId, + positionTitle: 'Engineer', + }), + ); + await answerRepo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory: category, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: answerValue, + }), + ); + } + + it('creates a report with aggregated analytics for a complete review', async () => { + const { reviewId, cycleId } = await seedReview(); + const position = (await positionRepo.search({} as any))[0]; + const { questionId } = await seedQuestionAndRelation( + reviewId, + cycleId, + ); + + await seedRespondentAndAnswer( + reviewId, + questionId, + position.id!, + RespondentCategory.TEAM, + 4, + ); + await seedRespondentAndAnswer( + reviewId, + questionId, + position.id!, + RespondentCategory.OTHER, + 3, + ); + + const created = await service.generateReportForReview(reviewId); + + expect(created.id).toBeDefined(); + expect(created.respondentCount).toBe(2); + expect(created.answerCount).toBe(2); + expect(created.cycleId).toBe(cycleId); + + // Analytics rows should have been written for the question + // and the competence (one of each at minimum). + const fromDb = await reportRepo.findById(created.id!); + expect(fromDb!.analytics.length).toBeGreaterThan(0); + + // The owning review should be linked back to the report. + const reviewAfter = await reviewRepo.findById(reviewId); + expect(reviewAfter!.reportId).toBe(created.id); + }); + + it('returns the existing report when one already exists for the review', async () => { + const { reviewId, cycleId } = await seedReview(); + const existingId = await seedReport(reviewId, cycleId); + + const result = await service.generateReportForReview(reviewId); + + expect(result.id).toBe(existingId); + }); + + it('throws NotFoundException when the review is missing', async () => { + await expect( + service.generateReportForReview(999999), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/service/startegic-report-insight.service.integration-spec.ts b/apps/api/test/integration/contexts/reporting/service/startegic-report-insight.service.integration-spec.ts new file mode 100644 index 00000000..0b8aed4b --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/service/startegic-report-insight.service.integration-spec.ts @@ -0,0 +1,214 @@ +import '../../../setup-env'; + +import { IdentityRole, InsightType, ReviewStage } from '@intra/shared-kernel'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { StartegicReportInsightService } from 'src/contexts/reporting/application/services/startegic-report-insight.service'; +import { StrategicReportInsightDomain } from 'src/contexts/reporting/domain/startegic-report-insight.domain'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { StrategicReportInsightRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository'; +import { StrategicReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('StartegicReportInsightService (integration)', () => { + let module: TestingModule; + let service: StartegicReportInsightService; + let insightsRepo: StrategicReportInsightRepository; + let strategicRepo: StrategicReportRepository; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let competenceRepo: CompetenceRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let hrId: number; + let strategicReportId: number; + let competenceId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + service = module.get(StartegicReportInsightService); + insightsRepo = module.get(StrategicReportInsightRepository); + strategicRepo = module.get(StrategicReportRepository); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + competenceRepo = module.get(CompetenceRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + // Strategic-report access check resolves reviews by cycleId + // before the role check — without one, an unprivileged actor + // would hit NotFoundException instead of ForbiddenException. + await reviewRepo.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const competence = await competenceRepo.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const strategicReport = await strategicRepo.create( + StrategicReportDomain.create({ + cycleId: cycle.id!, + cycleTitle: 'Cycle', + rateeCount: 1, + rateeIds: [], + respondentCount: 3, + respondentIds: [], + answerCount: 12, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 1, + competenceIds: [competence.id!], + questionCount: 0, + questionIds: [], + }), + ); + + hrId = hr.id!; + strategicReportId = strategicReport.id!; + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function hrActor(): UserDomain { + return UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + } + + async function seedInsights() { + await insightsRepo.createMany(strategicReportId, [ + StrategicReportInsightDomain.create({ + strategicReportId, + insightType: InsightType.HIGHEST_RATING, + competenceId, + competenceTitle: 'Teamwork', + averageScore: 4.6, + }), + ]); + } + + describe('getByStrategicReportId', () => { + it('returns insights tied to the strategic report for an HR actor', async () => { + await seedInsights(); + + const result = await service.getByStrategicReportId( + strategicReportId, + hrActor(), + ); + + expect(result).toHaveLength(1); + expect(result[0].insightType).toBe(InsightType.HIGHEST_RATING); + }); + + it('returns an empty array when no insights exist (access still checked)', async () => { + await expect( + service.getByStrategicReportId(strategicReportId, hrActor()), + ).resolves.toEqual([]); + }); + + it('throws NotFoundException when strategic report is missing', async () => { + await expect( + service.getByStrategicReportId(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('throws ForbiddenException for an unprivileged actor', async () => { + const otherId = ( + await identityUsers.create({ + firstName: 'X', + lastName: 'Y', + email: `x.${Date.now()}.${Math.random()}@example.com`, + } as any) + ).id!; + const stranger = UserDomain.create({ + id: otherId, + firstName: 'X', + lastName: 'Y', + email: 'x@example.com', + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.getByStrategicReportId(strategicReportId, stranger), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('getById', () => { + it('returns the insight when found and the actor has access', async () => { + await seedInsights(); + const stored = await prisma.strategicReportInsights.findFirst({ + where: { strategicReportId }, + }); + + const result = await service.getById(stored!.id, hrActor()); + + expect(result.id).toBe(stored!.id); + }); + + it('throws NotFoundException when insight id is missing', async () => { + await expect( + service.getById(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/service/strategic-report-analytics.service.integration-spec.ts b/apps/api/test/integration/contexts/reporting/service/strategic-report-analytics.service.integration-spec.ts new file mode 100644 index 00000000..67375a65 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/service/strategic-report-analytics.service.integration-spec.ts @@ -0,0 +1,214 @@ +import '../../../setup-env'; + +import { IdentityRole, ReviewStage } from '@intra/shared-kernel'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { StrategicReportAnalyticsService } from 'src/contexts/reporting/application/services/strategic-report-analytics.service'; +import { StrategicReportAnalyticsDomain } from 'src/contexts/reporting/domain/strategic-report-analytics.domain'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { StrategicReportAnalyticsRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository'; +import { StrategicReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('StrategicReportAnalyticsService (integration)', () => { + let module: TestingModule; + let service: StrategicReportAnalyticsService; + let analyticsRepo: StrategicReportAnalyticsRepository; + let strategicRepo: StrategicReportRepository; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let competenceRepo: CompetenceRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + let hrId: number; + let strategicReportId: number; + let competenceId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + service = module.get(StrategicReportAnalyticsService); + analyticsRepo = module.get(StrategicReportAnalyticsRepository); + strategicRepo = module.get(StrategicReportRepository); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + competenceRepo = module.get(CompetenceRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + // Strategic-report access check resolves reviews by cycleId + // before the role check — without one, an unprivileged actor + // would hit NotFoundException instead of ForbiddenException. + await reviewRepo.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const competence = await competenceRepo.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const strategicReport = await strategicRepo.create( + StrategicReportDomain.create({ + cycleId: cycle.id!, + cycleTitle: 'Cycle', + rateeCount: 1, + rateeIds: [], + respondentCount: 3, + respondentIds: [], + answerCount: 12, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 1, + competenceIds: [competence.id!], + questionCount: 0, + questionIds: [], + }), + ); + + hrId = hr.id!; + strategicReportId = strategicReport.id!; + competenceId = competence.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + function hrActor(): UserDomain { + return UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + } + + async function seedAnalytics() { + await analyticsRepo.createMany(strategicReportId, [ + StrategicReportAnalyticsDomain.create({ + strategicReportId, + competenceId, + competenceTitle: 'Teamwork', + averageByTeam: 3.5, + averageByOther: 4.0, + }), + ]); + } + + describe('getByStrategicReportId', () => { + it('returns analytics tied to the strategic report for an HR actor', async () => { + await seedAnalytics(); + + const result = await service.getByStrategicReportId( + strategicReportId, + hrActor(), + ); + + expect(result).toHaveLength(1); + expect(result[0].competenceTitle).toBe('Teamwork'); + }); + + it('returns an empty array when no analytics exist (access still checked)', async () => { + await expect( + service.getByStrategicReportId(strategicReportId, hrActor()), + ).resolves.toEqual([]); + }); + + it('throws NotFoundException when strategic report is missing', async () => { + await expect( + service.getByStrategicReportId(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('throws ForbiddenException for an unprivileged actor', async () => { + const otherId = ( + await identityUsers.create({ + firstName: 'X', + lastName: 'Y', + email: `x.${Date.now()}.${Math.random()}@example.com`, + } as any) + ).id!; + const stranger = UserDomain.create({ + id: otherId, + firstName: 'X', + lastName: 'Y', + email: 'x@example.com', + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.getByStrategicReportId(strategicReportId, stranger), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('getById', () => { + it('returns the analytics row when found and the actor has access', async () => { + await seedAnalytics(); + const stored = await prisma.strategicReportAnalytics.findFirst({ + where: { strategicReportId }, + }); + + const result = await service.getById(stored!.id, hrActor()); + + expect(result.id).toBe(stored!.id); + }); + + it('throws NotFoundException when analytics id is missing', async () => { + await expect( + service.getById(999999, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/service/strategic-reports.service.integration-spec.ts b/apps/api/test/integration/contexts/reporting/service/strategic-reports.service.integration-spec.ts new file mode 100644 index 00000000..a0f6286b --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/service/strategic-reports.service.integration-spec.ts @@ -0,0 +1,338 @@ +import '../../../setup-env'; + +import { + AnswerType, + EntityType, + IdentityRole, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { AnswerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { ReportAnalyticsRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-analytics.repository'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { StrategicReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('StrategicReportingService (integration)', () => { + let module: TestingModule; + let service: StrategicReportingService; + let strategicRepo: StrategicReportRepository; + let reportRepo: ReportRepository; + let reportAnalyticsRepo: ReportAnalyticsRepository; + let answerRepo: AnswerRepository; + let reviewRepo: ReviewRepository; + let cycleRepo: CycleRepository; + let questionRepo: QuestionRepository; + let competenceRepo: CompetenceRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let hrId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + service = module.get(StrategicReportingService); + strategicRepo = module.get(StrategicReportRepository); + reportRepo = module.get(ReportRepository); + reportAnalyticsRepo = module.get(ReportAnalyticsRepository); + answerRepo = module.get(AnswerRepository); + reviewRepo = module.get(ReviewRepository); + cycleRepo = module.get(CycleRepository); + questionRepo = module.get(QuestionRepository); + competenceRepo = module.get(CompetenceRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + hrId = hr.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function seedCycleAndReview( + title: string, + ): Promise<{ cycleId: number; reviewId: number; rateeId: number }> { + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title, + hrId, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + return { cycleId: cycle.id!, reviewId: review.id!, rateeId: ratee.id! }; + } + + /** + * Seeds a Report tied to (reviewId, cycleId). If `withCompetence` is + * true, also seeds a Competence (the strategic-report generator + * requires at least one competence on the cycle side for its + * insight rows to satisfy the competence FK), an associated Question, + * and a single ReportAnalytics row referencing the competence — that + * gives `buildStrategicAnalyticPayload` something to aggregate. + */ + async function seedReport( + reviewId: number, + cycleId: number, + withCompetence: boolean = false, + ): Promise<{ reportId: number; competenceId?: number }> { + const report = await reportRepo.create( + ReportDomain.create({ + reviewId, + cycleId, + respondentCount: 3, + respondentCategories: [ + RespondentCategory.TEAM, + RespondentCategory.OTHER, + ], + answerCount: 12, + turnoutPctOfTeam: 80, + turnoutPctOfOther: 50, + analytics: [], + }), + ); + + if (!withCompetence) { + return { reportId: report.id! }; + } + + const competence = await competenceRepo.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const question = await questionRepo.create( + QuestionDomain.create({ + cycleId, + title: 'Rate teamwork', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: competence.id!, + }), + ); + // The strategic-report generator discovers competences for a + // cycle by walking each review's answers → question → competence. + // Without at least one answer, `allCompetences` ends up empty and + // the insight rows fall back to `competenceId: -1` (FK violation). + await answerRepo.create( + AnswerDomain.create({ + reviewId, + questionId: question.id!, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }), + ); + await reportAnalyticsRepo.createMany(report.id!, [ + ReportAnalyticsDomain.create({ + reportId: report.id!, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: competence.id!, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4.5, + averageByTeam: 3.8, + averageByOther: 4.0, + }), + ]); + return { reportId: report.id!, competenceId: competence.id! }; + } + + function buildStrategicReport(cycleId: number): StrategicReportDomain { + return StrategicReportDomain.create({ + cycleId, + cycleTitle: 'Cycle', + rateeCount: 1, + rateeIds: [], + respondentCount: 3, + respondentIds: [], + answerCount: 12, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 1, + positionIds: [], + competenceCount: 0, + competenceIds: [], + questionCount: 0, + questionIds: [], + }); + } + + function hrActor(): UserDomain { + return UserDomain.create({ + id: hrId, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + } + + describe('search / checkAccessToAllStrategicReports', () => { + it('returns persisted strategic reports for an HR actor', async () => { + const { cycleId } = await seedCycleAndReview('Cycle A'); + await strategicRepo.create(buildStrategicReport(cycleId)); + + const result = await service.search({} as any, hrActor()); + + expect(result).toHaveLength(1); + }); + + it('throws ForbiddenException for an unprivileged actor', async () => { + const otherId = ( + await identityUsers.create({ + firstName: 'Other', + lastName: 'P', + email: `o.${Date.now()}.${Math.random()}@example.com`, + } as any) + ).id!; + const stranger = UserDomain.create({ + id: otherId, + firstName: 'X', + lastName: 'Y', + email: 'x@example.com', + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.search({} as any, stranger), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('getById / getByCycleId', () => { + it('getById returns the report for an HR actor', async () => { + const { cycleId } = await seedCycleAndReview('Cycle A'); + const created = await strategicRepo.create( + buildStrategicReport(cycleId), + ); + + const result = await service.getById(created.id!, hrActor()); + + expect(result.cycleId).toBe(cycleId); + }); + + it('getById throws NotFoundException when missing', async () => { + await expect(service.getById(999999)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('getByCycleId returns the report tied to a cycle', async () => { + const { cycleId } = await seedCycleAndReview('Cycle A'); + await strategicRepo.create(buildStrategicReport(cycleId)); + + const result = await service.getByCycleId(cycleId, hrActor()); + + expect(result.cycleId).toBe(cycleId); + }); + + it('getByCycleId throws NotFoundException when no report exists', async () => { + const { cycleId } = await seedCycleAndReview('Cycle A'); + + await expect( + service.getByCycleId(cycleId, hrActor()), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('generateStrategicReportForCycle', () => { + it('creates a strategic report aggregating individual reports for a cycle', async () => { + const { cycleId, reviewId } = await seedCycleAndReview('Cycle A'); + await seedReport(reviewId, cycleId, true); + + const created = + await service.generateStrategicReportForCycle(cycleId); + + expect(created.id).toBeDefined(); + expect(created.cycleId).toBe(cycleId); + expect(created.cycleTitle).toBe('Cycle A'); + expect(created.competenceCount).toBeGreaterThan(0); + + const fromDb = await strategicRepo.findByCycleId(cycleId); + expect(fromDb).not.toBeNull(); + expect(fromDb!.id).toBe(created.id); + }); + + it('returns the existing strategic report if one is already persisted', async () => { + const { cycleId, reviewId } = await seedCycleAndReview('Cycle A'); + await seedReport(reviewId, cycleId); + const existing = await strategicRepo.create( + buildStrategicReport(cycleId), + ); + + const result = + await service.generateStrategicReportForCycle(cycleId); + + expect(result.id).toBe(existing.id); + }); + + it('throws NotFoundException when the cycle is missing', async () => { + await expect( + service.generateStrategicReportForCycle(999999), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('throws NotFoundException when no individual reports exist for the cycle', async () => { + const { cycleId } = await seedCycleAndReview('Cycle A'); + + await expect( + service.generateStrategicReportForCycle(cycleId), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/service/text-answer.service.integration-spec.ts b/apps/api/test/integration/contexts/reporting/service/text-answer.service.integration-spec.ts new file mode 100644 index 00000000..e7ec96d0 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/service/text-answer.service.integration-spec.ts @@ -0,0 +1,290 @@ +import '../../../setup-env'; + +import { + AnswerType, + IdentityRole, + RespondentCategory, + ReviewStage, +} from '@intra/shared-kernel'; +import { ForbiddenException } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { AnswerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { ReviewQuestionRelationRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review-question-relation.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { TextAnswerService } from 'src/contexts/reporting/application/services/text-answer.service'; +import { PrismaService } from 'src/database/prisma.service'; +import { + createReportingTestModule, + resetReportingTables, +} from '../test-app-reporting'; + +describe('TextAnswerService (integration)', () => { + let module: TestingModule; + let service: TextAnswerService; + let cycleRepo: CycleRepository; + let reviewRepo: ReviewRepository; + let questionRepo: QuestionRepository; + let answerRepo: AnswerRepository; + let relationRepo: ReviewQuestionRelationRepository; + let competenceRepo: CompetenceRepository; + let positionRepo: PositionRepository; + let identityUsers: IdentityUserService; + let prisma: PrismaService; + + let reviewId: number; + let textQuestionId: number; + let numericalQuestionId: number; + + beforeAll(async () => { + module = await createReportingTestModule(); + service = module.get(TextAnswerService); + cycleRepo = module.get(CycleRepository); + reviewRepo = module.get(ReviewRepository); + questionRepo = module.get(QuestionRepository); + answerRepo = module.get(AnswerRepository); + relationRepo = module.get(ReviewQuestionRelationRepository); + competenceRepo = module.get(CompetenceRepository); + positionRepo = module.get(PositionRepository); + identityUsers = module.get(IdentityUserService); + prisma = module.get(PrismaService); + }); + + beforeEach(async () => { + await resetReportingTables(prisma); + const hr = await identityUsers.create({ + firstName: 'Helena', + lastName: 'Reed', + email: `hr.${Date.now()}.${Math.random()}@example.com`, + } as any); + const ratee = await identityUsers.create({ + firstName: 'Robert', + lastName: 'Smith', + email: `ratee.${Date.now()}.${Math.random()}@example.com`, + } as any); + const position = await positionRepo.create( + PositionDomain.create({ title: 'Engineer' }), + ); + const cycle = await cycleRepo.create( + CycleDomain.create({ + title: 'Cycle', + hrId: hr.id!, + startDate: new Date('2026-01-01'), + endDate: new Date('2026-03-31'), + }), + ); + const review = await reviewRepo.create( + ReviewDomain.create({ + rateeId: ratee.id!, + rateeFullName: 'Robert Smith', + rateePositionId: position.id!, + rateePositionTitle: 'Engineer', + hrId: hr.id!, + hrFullName: 'Helena Reed', + cycleId: cycle.id!, + stage: ReviewStage.FINISHED, + }), + ); + const competence = await competenceRepo.create( + CompetenceDomain.create({ title: 'Teamwork' }), + ); + const textQuestion = await questionRepo.create( + QuestionDomain.create({ + cycleId: cycle.id!, + title: 'Free-form feedback', + answerType: AnswerType.TEXT_FIELD, + competenceId: competence.id!, + }), + ); + const numericalQuestion = await questionRepo.create( + QuestionDomain.create({ + cycleId: cycle.id!, + title: 'Rate teamwork', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: competence.id!, + }), + ); + // Relations supply the questionTitle the service maps back onto. + await relationRepo.link( + ReviewQuestionRelationDomain.create({ + reviewId: review.id!, + questionId: textQuestion.id!, + questionTitle: 'Free-form feedback', + answerType: AnswerType.TEXT_FIELD, + competenceId: competence.id!, + competenceTitle: 'Teamwork', + }), + ); + + reviewId = review.id!; + textQuestionId = textQuestion.id!; + numericalQuestionId = numericalQuestion.id!; + }); + + afterAll(async () => { + await module.close(); + }); + + async function seedAnswer( + questionId: number, + respondentCategory: RespondentCategory, + answerType: AnswerType, + textValue: string | null, + numericalValue: number | null = null, + ) { + await answerRepo.create( + AnswerDomain.create({ + reviewId, + questionId, + respondentCategory, + answerType, + textValue, + numericalValue, + }), + ); + } + + function hrActor(): UserDomain { + return UserDomain.create({ + id: 999, + firstName: 'H', + lastName: 'R', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + } + + function adminActor(): UserDomain { + return UserDomain.create({ + id: 1, + firstName: 'A', + lastName: 'D', + email: 'admin@example.com', + roles: [IdentityRole.ADMIN], + }); + } + + describe('listByReview', () => { + it('returns text answers with the question title mapped from relations (HR actor)', async () => { + await seedAnswer( + textQuestionId, + RespondentCategory.TEAM, + AnswerType.TEXT_FIELD, + 'Great team player', + ); + + const result = await service.listByReview(reviewId, hrActor()); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + questionId: textQuestionId, + questionTitle: 'Free-form feedback', + respondentCategory: RespondentCategory.TEAM, + textValue: 'Great team player', + }); + }); + + it('also allows ADMIN actors', async () => { + await seedAnswer( + textQuestionId, + RespondentCategory.OTHER, + AnswerType.TEXT_FIELD, + 'Good listener', + ); + + const result = await service.listByReview(reviewId, adminActor()); + expect(result).toHaveLength(1); + }); + + it('allows callers without an actor (system caller)', async () => { + await seedAnswer( + textQuestionId, + RespondentCategory.TEAM, + AnswerType.TEXT_FIELD, + 'system-readable', + ); + + const result = await service.listByReview(reviewId); + expect(result).toHaveLength(1); + }); + + it('throws ForbiddenException for a non-HR / non-Admin actor', async () => { + const employee = UserDomain.create({ + id: 5, + firstName: 'E', + lastName: 'M', + email: 'e@example.com', + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.listByReview(reviewId, employee), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + + it('filters out numerical answers and answers with empty/whitespace text', async () => { + // Numerical: should be excluded by the answerType filter at + // the repository level. + await seedAnswer( + numericalQuestionId, + RespondentCategory.TEAM, + AnswerType.NUMERICAL_SCALE, + null, + 4, + ); + // Empty string + whitespace-only: filtered out in the service. + await seedAnswer( + textQuestionId, + RespondentCategory.TEAM, + AnswerType.TEXT_FIELD, + '', + ); + await seedAnswer( + textQuestionId, + RespondentCategory.OTHER, + AnswerType.TEXT_FIELD, + ' ', + ); + await seedAnswer( + textQuestionId, + RespondentCategory.OTHER, + AnswerType.TEXT_FIELD, + 'Real feedback', + ); + + const result = await service.listByReview(reviewId, hrActor()); + + expect(result).toHaveLength(1); + expect(result[0].textValue).toBe('Real feedback'); + }); + + it('returns null for questionTitle when no relation exists for the question', async () => { + // Create an answer for a question that is NOT linked via + // a review-question relation. + await seedAnswer( + numericalQuestionId, + RespondentCategory.OTHER, + AnswerType.TEXT_FIELD, + 'Untitled answer', + ); + + const result = await service.listByReview(reviewId, hrActor()); + + expect(result).toHaveLength(1); + expect(result[0].questionTitle).toBeNull(); + expect(result[0].questionId).toBe(numericalQuestionId); + }); + }); +}); diff --git a/apps/api/test/integration/contexts/reporting/test-app-reporting.ts b/apps/api/test/integration/contexts/reporting/test-app-reporting.ts new file mode 100644 index 00000000..1be3e8c1 --- /dev/null +++ b/apps/api/test/integration/contexts/reporting/test-app-reporting.ts @@ -0,0 +1,309 @@ +import { ConfigModule } from '@nestjs/config'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { Test, TestingModule } from '@nestjs/testing'; +import appConfig from 'src/config/app'; +import databaseConfig from 'src/config/database'; +import { ANSWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/answer.repository.port'; +import { CLUSTER_SCORE_ANALYTICS_REPOSITORY } from 'src/contexts/feedback360/application/ports/cluster-score-analytics.repository.port'; +import { CLUSTER_SCORE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cluster-score.repository.port'; +import { CYCLE_STAGE_HISTORY_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle-stage-history.repository.port'; +import { CYCLE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle.repository.port'; +import { QUESTION_REPOSITORY } from 'src/contexts/feedback360/application/ports/question.repository.port'; +import { RESPONDENT_REPOSITORY } from 'src/contexts/feedback360/application/ports/respondent.repository.port'; +import { REVIEW_QUESTION_RELATION_REPOSITORY } from 'src/contexts/feedback360/application/ports/review-question-relation.repository.port'; +import { REVIEW_STAGE_HISTORY_REPOSITORY } from 'src/contexts/feedback360/application/ports/review-stage-history.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { REVIEWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/reviewer.repository.port'; +import { ClusterScoreAnalyticsService } from 'src/contexts/feedback360/application/services/cluster-score-analytics.service'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { AnswerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository'; +import { ClusterScoreAnalyticsRepository as Feedback360ClusterScoreAnalyticsRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository'; +import { ClusterScoreRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score.repository'; +import { CycleStageHistoryRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository'; +import { CycleRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository'; +import { QuestionRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/question.repository'; +import { RespondentRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository'; +import { ReviewQuestionRelationRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review-question-relation.repository'; +import { ReviewStageHistoryRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review-stage-history.repository'; +import { ReviewRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/review.repository'; +import { ReviewerRepository } from 'src/contexts/feedback360/infrastructure/prisma-repositories/reviewer.repository'; +import { IDENTITY_ROLE_REPOSITORY } from 'src/contexts/identity/application/ports/role.repository.port'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { RoleRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/role.repository'; +import { UserRepository } from 'src/contexts/identity/infrastructure/prisma-repositories/user.repository'; +import { CLUSTER_REPOSITORY } from 'src/contexts/library/application/ports/cluster.repository.port'; +import { COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/competence-question-template-relation.repository.port'; +import { COMPETENCE_REPOSITORY } from 'src/contexts/library/application/ports/competence.repository.port'; +import { POSITION_COMPETENCE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-competence-relation.repository.port'; +import { POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-question-template-relation.repository.port'; +import { QUESTION_TEMPLATE_REPOSITORY } from 'src/contexts/library/application/ports/question-template.repository.port'; +import { ClusterService } from 'src/contexts/library/application/services/cluster.service'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service'; +import { ClusterRepository } from 'src/contexts/library/infrastructure/prisma-repositories/cluster.repository'; +import { CompetenceQuestionTemplateRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence-question-template-relation.repository'; +import { CompetenceRepository } from 'src/contexts/library/infrastructure/prisma-repositories/competence.repository'; +import { PositionCompetenceRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-competence-relation.repository'; +import { PositionQuestionTemplateRelationRepository } from 'src/contexts/library/infrastructure/prisma-repositories/position-question-template-relation.repository'; +import { QuestionTemplateRepository } from 'src/contexts/library/infrastructure/prisma-repositories/question-template.repository'; +import { ORGANISATION_POSITION_REPOSITORY } from 'src/contexts/organisation/application/ports/position.repository.port'; +import { ORGANISATION_TEAM_REPOSITORY } from 'src/contexts/organisation/application/ports/team.repository.port'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PositionRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/position.repository'; +import { TeamRepository } from 'src/contexts/organisation/infrastructure/prisma-repositories/team.repository'; +import { CycleStageListener as ReportingCycleStageListener } from 'src/contexts/reporting/application/listeners/cycle-stage.listener'; +import { ReviewStageListener as ReportingReviewStageListener } from 'src/contexts/reporting/application/listeners/review-stage.listener'; +import { REPORT_ANALYTICS_REPOSITORY } from 'src/contexts/reporting/application/ports/report-analytics.repository.port'; +import { REPORT_COMMENT_REPOSITORY } from 'src/contexts/reporting/application/ports/report-comment.repository.port'; +import { REPORT_INSIGHT_REPOSITORY } from 'src/contexts/reporting/application/ports/report-insight.repository.port'; +import { REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/report.repository.port'; +import { STRATEGIC_REPORT_ANALYTICS_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report-analytics.repository.port'; +import { STRATEGIC_REPORT_INSIGHT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report-insight.repository.port'; +import { STRATEGIC_REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report.repository.port'; +import { ReportAnalyticsService } from 'src/contexts/reporting/application/services/report-analytics.service'; +import { ReportCommentService } from 'src/contexts/reporting/application/services/report-comment.service'; +import { ReportInsightService } from 'src/contexts/reporting/application/services/report-insight.service'; +import { ReportingService } from 'src/contexts/reporting/application/services/reports.service'; +import { StartegicReportInsightService } from 'src/contexts/reporting/application/services/startegic-report-insight.service'; +import { StrategicReportAnalyticsService } from 'src/contexts/reporting/application/services/strategic-report-analytics.service'; +import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service'; +import { TextAnswerService } from 'src/contexts/reporting/application/services/text-answer.service'; +import { ReportAnalyticsRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-analytics.repository'; +import { ReportCommentRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-comment.repository'; +import { ReportInsightRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report-insight.repository'; +import { ReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/report.repository'; +import { StrategicReportInsightRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository'; +import { StrategicReportAnalyticsRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository'; +import { StrategicReportRepository } from 'src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository'; +import { DatabaseModule } from 'src/database/database.module'; +import { PrismaService } from 'src/database/prisma.service'; +import { ensureTestDatabaseExists } from '../../setup-env'; + +/** + * Bootstraps a Nest testing module for the reporting context wired to + * the real database. We assemble the providers manually instead of + * importing `ReportingModule` because that module pulls in + * `AuthModule`, `Feedback360Module` (via forwardRef), and a fleet of + * listeners that require the full HTTP / better-auth setup. + * + * Cross-context dependencies wired in: + * - Identity (`IdentityUserService` + repo) so reviews can reference + * real `identity_users` rows for HR, ratee, etc. + * - Organisation (`PositionRepository`, `TeamRepository`) so reviews + * can reference real positions/teams without FK errors. + * - Library (`CompetenceRepository`, `ClusterRepository`) because + * reporting analytics/insights reference competences. + * - Feedback360 (`CycleRepository`, `ReviewRepository`, + * `QuestionRepository`, `RespondentRepository`, `ReviewerRepository`, + * `AnswerRepository`) so reports can be tied to real reviews / cycles + * / questions when the foreign-key constraints fire. + */ +export async function createReportingTestModule(): Promise { + await ensureTestDatabaseExists(); + + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [appConfig, databaseConfig], + ignoreEnvFile: true, + }), + EventEmitterModule.forRoot(), + DatabaseModule, + ], + providers: [ + // Reporting — services + ReportingService, + StrategicReportingService, + ReportAnalyticsService, + ReportInsightService, + ReportCommentService, + StrategicReportAnalyticsService, + StartegicReportInsightService, + TextAnswerService, + // Reporting — listeners + ReportingCycleStageListener, + ReportingReviewStageListener, + // Reporting — repositories + ReportRepository, + ReportAnalyticsRepository, + ReportInsightRepository, + ReportCommentRepository, + StrategicReportRepository, + StrategicReportAnalyticsRepository, + StrategicReportInsightRepository, + { provide: REPORT_REPOSITORY, useExisting: ReportRepository }, + { + provide: REPORT_ANALYTICS_REPOSITORY, + useExisting: ReportAnalyticsRepository, + }, + { + provide: REPORT_INSIGHT_REPOSITORY, + useExisting: ReportInsightRepository, + }, + { + provide: REPORT_COMMENT_REPOSITORY, + useExisting: ReportCommentRepository, + }, + { + provide: STRATEGIC_REPORT_REPOSITORY, + useExisting: StrategicReportRepository, + }, + { + provide: STRATEGIC_REPORT_ANALYTICS_REPOSITORY, + useExisting: StrategicReportAnalyticsRepository, + }, + { + provide: STRATEGIC_REPORT_INSIGHT_REPOSITORY, + useExisting: StrategicReportInsightRepository, + }, + // Feedback360 — services (required by reporting listeners + // which call CycleService.changeStage / ReviewService.changeReviewStage) + CycleService, + ReviewService, + ClusterScoreAnalyticsService, + // Feedback360 — repositories + CycleRepository, + ReviewRepository, + QuestionRepository, + RespondentRepository, + ReviewerRepository, + AnswerRepository, + ClusterScoreRepository, + ReviewQuestionRelationRepository, + CycleStageHistoryRepository, + ReviewStageHistoryRepository, + Feedback360ClusterScoreAnalyticsRepository, + { provide: CYCLE_REPOSITORY, useExisting: CycleRepository }, + { provide: REVIEW_REPOSITORY, useExisting: ReviewRepository }, + { provide: QUESTION_REPOSITORY, useExisting: QuestionRepository }, + { + provide: RESPONDENT_REPOSITORY, + useExisting: RespondentRepository, + }, + { provide: REVIEWER_REPOSITORY, useExisting: ReviewerRepository }, + { provide: ANSWER_REPOSITORY, useExisting: AnswerRepository }, + { + provide: CLUSTER_SCORE_REPOSITORY, + useExisting: ClusterScoreRepository, + }, + { + provide: REVIEW_QUESTION_RELATION_REPOSITORY, + useExisting: ReviewQuestionRelationRepository, + }, + { + provide: CYCLE_STAGE_HISTORY_REPOSITORY, + useExisting: CycleStageHistoryRepository, + }, + { + provide: REVIEW_STAGE_HISTORY_REPOSITORY, + useExisting: ReviewStageHistoryRepository, + }, + { + provide: CLUSTER_SCORE_ANALYTICS_REPOSITORY, + useExisting: Feedback360ClusterScoreAnalyticsRepository, + }, + // Library — services + relation repos required by ReviewService + CompetenceService, + ClusterService, + QuestionTemplateService, + CompetenceRepository, + ClusterRepository, + QuestionTemplateRepository, + PositionCompetenceRelationRepository, + PositionQuestionTemplateRelationRepository, + CompetenceQuestionTemplateRelationRepository, + { + provide: COMPETENCE_REPOSITORY, + useExisting: CompetenceRepository, + }, + { provide: CLUSTER_REPOSITORY, useExisting: ClusterRepository }, + { + provide: QUESTION_TEMPLATE_REPOSITORY, + useExisting: QuestionTemplateRepository, + }, + { + provide: POSITION_COMPETENCE_RELATION_REPOSITORY, + useExisting: PositionCompetenceRelationRepository, + }, + { + provide: POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useExisting: PositionQuestionTemplateRelationRepository, + }, + { + provide: COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useExisting: CompetenceQuestionTemplateRelationRepository, + }, + // Organisation + PositionService, + PositionRepository, + TeamRepository, + { + provide: ORGANISATION_POSITION_REPOSITORY, + useExisting: PositionRepository, + }, + { + provide: ORGANISATION_TEAM_REPOSITORY, + useExisting: TeamRepository, + }, + // Identity + IdentityUserService, + IdentityRoleService, + UserRepository, + RoleRepository, + { provide: IDENTITY_USER_REPOSITORY, useExisting: UserRepository }, + { provide: IDENTITY_ROLE_REPOSITORY, useExisting: RoleRepository }, + ], + }).compile(); + + await module.init(); + return module; +} + +/** + * Truncates every table touched by reporting scenarios. The order is + * not significant because we use CASCADE, but the explicit list + * documents the schema dependencies. `identity_roles` (reference data) + * is preserved. + */ +export async function resetReportingTables( + prisma: PrismaService, +): Promise { + await prisma.$executeRawUnsafe( + `TRUNCATE TABLE + "reporting_strategic_report_insights", + "reporting_strategic_report_analytics", + "reporting_strategic_reports", + "reporting_individual_report_comments", + "reporting_individual_report_insights", + "reporting_individual_report_analytics", + "reporting_individual_reports", + "feedback360_answers", + "feedback360_cluster_score_analytics", + "feedback360_cluster_scores", + "feedback360_review_question_relations", + "feedback360_review_stage_history", + "feedback360_cycle_stage_history", + "feedback360_respondents", + "feedback360_reviewers", + "feedback360_questions", + "feedback360_reviews", + "feedback360_cycles", + "library_clusters", + "library_position_question_template_relations", + "library_position_competence_relations", + "library_competence_question_template_relations", + "library_question_templates", + "library_competences", + "org_team_memberships", + "org_teams", + "org_position_hierarchies", + "org_positions", + "identity_users_roles", + "identity_users" + RESTART IDENTITY CASCADE`, + ); +} diff --git a/apps/api/test/integration/coverage/clover.xml b/apps/api/test/integration/coverage/clover.xml new file mode 100644 index 00000000..cadd0974 --- /dev/null +++ b/apps/api/test/integration/coverage/clover.xml @@ -0,0 +1,2649 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/api/test/integration/coverage/coverage-final.json b/apps/api/test/integration/coverage/coverage-final.json new file mode 100644 index 00000000..271a9459 --- /dev/null +++ b/apps/api/test/integration/coverage/coverage-final.json @@ -0,0 +1,60 @@ +{"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/cycle-stage.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/cycle-stage.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":50}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":63}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":107}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":121}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":81}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"7":{"start":{"line":18,"column":31},"end":{"line":85,"column":null}},"8":{"start":{"line":19,"column":30},"end":{"line":19,"column":65}},"9":{"start":{"line":22,"column":25},"end":{"line":22,"column":36}},"10":{"start":{"line":23,"column":25},"end":{"line":23,"column":39}},"11":{"start":{"line":25,"column":25},"end":{"line":25,"column":34}},"12":{"start":{"line":37,"column":8},"end":{"line":39,"column":10}},"13":{"start":{"line":41,"column":8},"end":{"line":48,"column":9}},"14":{"start":{"line":43,"column":12},"end":{"line":46,"column":14}},"15":{"start":{"line":47,"column":12},"end":{"line":47,"column":19}},"16":{"start":{"line":50,"column":24},"end":{"line":50,"column":71}},"17":{"start":{"line":52,"column":8},"end":{"line":61,"column":9}},"18":{"start":{"line":53,"column":12},"end":{"line":55,"column":14}},"19":{"start":{"line":56,"column":12},"end":{"line":59,"column":14}},"20":{"start":{"line":60,"column":12},"end":{"line":60,"column":19}},"21":{"start":{"line":63,"column":8},"end":{"line":65,"column":10}},"22":{"start":{"line":67,"column":8},"end":{"line":83,"column":9}},"23":{"start":{"line":68,"column":12},"end":{"line":68,"column":74}},"24":{"start":{"line":70,"column":12},"end":{"line":72,"column":14}},"25":{"start":{"line":74,"column":12},"end":{"line":76,"column":14}},"26":{"start":{"line":79,"column":12},"end":{"line":82,"column":14}},"27":{"start":{"line":18,"column":13},"end":{"line":18,"column":31}},"28":{"start":{"line":34,"column":10},"end":{"line":84,"column":null}},"29":{"start":{"line":18,"column":13},"end":{"line":85,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"loc":{"start":{"line":25,"column":54},"end":{"line":26,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":34,"column":10},"end":{"line":34,"column":33}},"loc":{"start":{"line":35,"column":37},"end":{"line":84,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":41,"column":8},"end":{"line":48,"column":9}},"type":"if","locations":[{"start":{"line":41,"column":8},"end":{"line":48,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":52,"column":8},"end":{"line":61,"column":9}},"type":"if","locations":[{"start":{"line":52,"column":8},"end":{"line":61,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":35,"column":15},"end":{"line":35,"column":null}},"type":"cond-expr","locations":[{"start":{"line":35,"column":37},"end":{"line":35,"column":null}},{"start":{"line":35,"column":37},"end":{"line":35,"column":null}}]},"3":{"loc":{"start":{"line":35,"column":15},"end":{"line":35,"column":37}},"type":"binary-expr","locations":[{"start":{"line":35,"column":15},"end":{"line":35,"column":null}},{"start":{"line":35,"column":15},"end":{"line":35,"column":37}}]},"4":{"loc":{"start":{"line":36,"column":7},"end":{"line":36,"column":null}},"type":"cond-expr","locations":[{"start":{"line":36,"column":14},"end":{"line":36,"column":null}},{"start":{"line":36,"column":14},"end":{"line":36,"column":null}}]},"5":{"loc":{"start":{"line":36,"column":7},"end":{"line":36,"column":14}},"type":"binary-expr","locations":[{"start":{"line":36,"column":7},"end":{"line":36,"column":null}},{"start":{"line":36,"column":7},"end":{"line":36,"column":14}}]},"6":{"loc":{"start":{"line":22,"column":36},"end":{"line":22,"column":null}},"type":"cond-expr","locations":[{"start":{"line":22,"column":64},"end":{"line":22,"column":null}},{"start":{"line":22,"column":64},"end":{"line":22,"column":null}}]},"7":{"loc":{"start":{"line":22,"column":36},"end":{"line":22,"column":64}},"type":"binary-expr","locations":[{"start":{"line":22,"column":36},"end":{"line":22,"column":null}},{"start":{"line":22,"column":36},"end":{"line":22,"column":64}}]},"8":{"loc":{"start":{"line":22,"column":64},"end":{"line":23,"column":null}},"type":"cond-expr","locations":[{"start":{"line":23,"column":52},"end":{"line":23,"column":null}},{"start":{"line":23,"column":52},"end":{"line":23,"column":null}}]},"9":{"loc":{"start":{"line":22,"column":64},"end":{"line":23,"column":52}},"type":"binary-expr","locations":[{"start":{"line":22,"column":64},"end":{"line":23,"column":null}},{"start":{"line":23,"column":39},"end":{"line":23,"column":52}}]},"10":{"loc":{"start":{"line":23,"column":52},"end":{"line":25,"column":null}},"type":"cond-expr","locations":[{"start":{"line":25,"column":54},"end":{"line":25,"column":null}},{"start":{"line":25,"column":54},"end":{"line":25,"column":null}}]},"11":{"loc":{"start":{"line":23,"column":52},"end":{"line":25,"column":54}},"type":"binary-expr","locations":[{"start":{"line":23,"column":52},"end":{"line":25,"column":null}},{"start":{"line":25,"column":34},"end":{"line":25,"column":54}}]}},"s":{"0":18,"1":18,"2":18,"3":18,"4":18,"5":18,"6":18,"7":18,"8":18,"9":18,"10":18,"11":18,"12":14,"13":14,"14":8,"15":8,"16":6,"17":6,"18":2,"19":2,"20":2,"21":4,"22":4,"23":4,"24":4,"25":0,"26":4,"27":18,"28":18,"29":18},"f":{"0":18,"1":14},"b":{"0":[8,6],"1":[2,4],"2":[18,0],"3":[18,18],"4":[18,0],"5":[18,18],"6":[18,0],"7":[18,18],"8":[18,0],"9":[18,18],"10":[0,18],"11":[18,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/respondent-status.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/respondent-status.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":6,"column":0},"end":{"line":6,"column":60}},"2":{"start":{"line":7,"column":0},"end":{"line":7,"column":63}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":75}},"4":{"start":{"line":9,"column":0},"end":{"line":9,"column":89}},"5":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"6":{"start":{"line":14,"column":0},"end":{"line":14,"column":null}},"7":{"start":{"line":18,"column":0},"end":{"line":18,"column":59}},"8":{"start":{"line":25,"column":37},"end":{"line":95,"column":null}},"9":{"start":{"line":26,"column":30},"end":{"line":26,"column":71}},"10":{"start":{"line":30,"column":25},"end":{"line":30,"column":38}},"11":{"start":{"line":31,"column":25},"end":{"line":31,"column":34}},"12":{"start":{"line":32,"column":25},"end":{"line":32,"column":39}},"13":{"start":{"line":44,"column":8},"end":{"line":46,"column":10}},"14":{"start":{"line":48,"column":8},"end":{"line":53,"column":9}},"15":{"start":{"line":52,"column":12},"end":{"line":52,"column":19}},"16":{"start":{"line":55,"column":8},"end":{"line":57,"column":10}},"17":{"start":{"line":59,"column":8},"end":{"line":93,"column":9}},"18":{"start":{"line":60,"column":32},"end":{"line":62,"column":null}},"19":{"start":{"line":64,"column":33},"end":{"line":67,"column":null}},"20":{"start":{"line":66,"column":20},"end":{"line":67,"column":73}},"21":{"start":{"line":70,"column":12},"end":{"line":87,"column":13}},"22":{"start":{"line":71,"column":16},"end":{"line":71,"column":66}},"23":{"start":{"line":72,"column":16},"end":{"line":74,"column":18}},"24":{"start":{"line":76,"column":16},"end":{"line":86,"column":18}},"25":{"start":{"line":80,"column":32},"end":{"line":83,"column":62}},"26":{"start":{"line":89,"column":12},"end":{"line":92,"column":14}},"27":{"start":{"line":25,"column":13},"end":{"line":25,"column":37}},"28":{"start":{"line":41,"column":10},"end":{"line":94,"column":null}},"29":{"start":{"line":25,"column":13},"end":{"line":95,"column":null}},"30":{"start":{"line":106,"column":44},"end":{"line":166,"column":null}},"31":{"start":{"line":107,"column":30},"end":{"line":107,"column":78}},"32":{"start":{"line":111,"column":25},"end":{"line":111,"column":38}},"33":{"start":{"line":113,"column":25},"end":{"line":113,"column":34}},"34":{"start":{"line":114,"column":25},"end":{"line":114,"column":40}},"35":{"start":{"line":121,"column":8},"end":{"line":123,"column":9}},"36":{"start":{"line":122,"column":12},"end":{"line":122,"column":19}},"37":{"start":{"line":125,"column":8},"end":{"line":164,"column":9}},"38":{"start":{"line":126,"column":31},"end":{"line":127,"column":null}},"39":{"start":{"line":130,"column":12},"end":{"line":132,"column":13}},"40":{"start":{"line":131,"column":16},"end":{"line":131,"column":23}},"41":{"start":{"line":134,"column":27},"end":{"line":134,"column":70}},"42":{"start":{"line":135,"column":12},"end":{"line":140,"column":13}},"43":{"start":{"line":136,"column":16},"end":{"line":138,"column":18}},"44":{"start":{"line":139,"column":16},"end":{"line":139,"column":23}},"45":{"start":{"line":142,"column":12},"end":{"line":144,"column":13}},"46":{"start":{"line":143,"column":16},"end":{"line":143,"column":23}},"47":{"start":{"line":146,"column":12},"end":{"line":148,"column":14}},"48":{"start":{"line":150,"column":12},"end":{"line":156,"column":14}},"49":{"start":{"line":158,"column":12},"end":{"line":163,"column":14}},"50":{"start":{"line":106,"column":13},"end":{"line":106,"column":44}},"51":{"start":{"line":118,"column":10},"end":{"line":165,"column":null}},"52":{"start":{"line":106,"column":13},"end":{"line":166,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"loc":{"start":{"line":32,"column":52},"end":{"line":33,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":41,"column":10},"end":{"line":41,"column":39}},"loc":{"start":{"line":42,"column":43},"end":{"line":94,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":65,"column":16},"end":{"line":65,"column":17}},"loc":{"start":{"line":66,"column":20},"end":{"line":67,"column":73}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":79,"column":28},"end":{"line":79,"column":29}},"loc":{"start":{"line":80,"column":32},"end":{"line":83,"column":62}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":109,"column":4},"end":{"line":109,"column":null}},"loc":{"start":{"line":114,"column":53},"end":{"line":115,"column":8}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":118,"column":10},"end":{"line":118,"column":35}},"loc":{"start":{"line":119,"column":43},"end":{"line":165,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":48,"column":8},"end":{"line":53,"column":9}},"type":"if","locations":[{"start":{"line":48,"column":8},"end":{"line":53,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":49,"column":12},"end":{"line":50,"column":54}},"type":"binary-expr","locations":[{"start":{"line":49,"column":12},"end":{"line":49,"column":55}},{"start":{"line":50,"column":12},"end":{"line":50,"column":54}}]},"2":{"loc":{"start":{"line":66,"column":20},"end":{"line":67,"column":73}},"type":"binary-expr","locations":[{"start":{"line":66,"column":20},"end":{"line":66,"column":74}},{"start":{"line":67,"column":20},"end":{"line":67,"column":73}}]},"3":{"loc":{"start":{"line":70,"column":12},"end":{"line":87,"column":13}},"type":"if","locations":[{"start":{"line":70,"column":12},"end":{"line":87,"column":13}},{"start":{"line":75,"column":19},"end":{"line":87,"column":13}}]},"4":{"loc":{"start":{"line":80,"column":32},"end":{"line":83,"column":62}},"type":"binary-expr","locations":[{"start":{"line":80,"column":32},"end":{"line":81,"column":58}},{"start":{"line":82,"column":32},"end":{"line":83,"column":62}}]},"5":{"loc":{"start":{"line":42,"column":15},"end":{"line":42,"column":null}},"type":"cond-expr","locations":[{"start":{"line":42,"column":43},"end":{"line":42,"column":null}},{"start":{"line":42,"column":43},"end":{"line":42,"column":null}}]},"6":{"loc":{"start":{"line":42,"column":15},"end":{"line":42,"column":43}},"type":"binary-expr","locations":[{"start":{"line":42,"column":15},"end":{"line":42,"column":null}},{"start":{"line":42,"column":15},"end":{"line":42,"column":43}}]},"7":{"loc":{"start":{"line":43,"column":7},"end":{"line":43,"column":null}},"type":"cond-expr","locations":[{"start":{"line":43,"column":14},"end":{"line":43,"column":null}},{"start":{"line":43,"column":14},"end":{"line":43,"column":null}}]},"8":{"loc":{"start":{"line":43,"column":7},"end":{"line":43,"column":14}},"type":"binary-expr","locations":[{"start":{"line":43,"column":7},"end":{"line":43,"column":null}},{"start":{"line":43,"column":7},"end":{"line":43,"column":14}}]},"9":{"loc":{"start":{"line":30,"column":38},"end":{"line":30,"column":null}},"type":"cond-expr","locations":[{"start":{"line":30,"column":62},"end":{"line":30,"column":null}},{"start":{"line":30,"column":62},"end":{"line":30,"column":null}}]},"10":{"loc":{"start":{"line":30,"column":38},"end":{"line":30,"column":62}},"type":"binary-expr","locations":[{"start":{"line":30,"column":38},"end":{"line":30,"column":null}},{"start":{"line":30,"column":38},"end":{"line":30,"column":62}}]},"11":{"loc":{"start":{"line":30,"column":62},"end":{"line":31,"column":null}},"type":"cond-expr","locations":[{"start":{"line":31,"column":47},"end":{"line":31,"column":null}},{"start":{"line":31,"column":47},"end":{"line":31,"column":null}}]},"12":{"loc":{"start":{"line":30,"column":62},"end":{"line":31,"column":47}},"type":"binary-expr","locations":[{"start":{"line":30,"column":62},"end":{"line":31,"column":null}},{"start":{"line":31,"column":34},"end":{"line":31,"column":47}}]},"13":{"loc":{"start":{"line":31,"column":47},"end":{"line":32,"column":null}},"type":"cond-expr","locations":[{"start":{"line":32,"column":52},"end":{"line":32,"column":null}},{"start":{"line":32,"column":52},"end":{"line":32,"column":null}}]},"14":{"loc":{"start":{"line":31,"column":47},"end":{"line":32,"column":52}},"type":"binary-expr","locations":[{"start":{"line":31,"column":47},"end":{"line":32,"column":null}},{"start":{"line":32,"column":39},"end":{"line":32,"column":52}}]},"15":{"loc":{"start":{"line":121,"column":8},"end":{"line":123,"column":9}},"type":"if","locations":[{"start":{"line":121,"column":8},"end":{"line":123,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":130,"column":12},"end":{"line":132,"column":13}},"type":"if","locations":[{"start":{"line":130,"column":12},"end":{"line":132,"column":13}},{"start":{},"end":{}}]},"17":{"loc":{"start":{"line":135,"column":12},"end":{"line":140,"column":13}},"type":"if","locations":[{"start":{"line":135,"column":12},"end":{"line":140,"column":13}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":142,"column":12},"end":{"line":144,"column":13}},"type":"if","locations":[{"start":{"line":142,"column":12},"end":{"line":144,"column":13}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":153,"column":16},"end":{"line":153,"column":46}},"type":"binary-expr","locations":[{"start":{"line":153,"column":16},"end":{"line":153,"column":33}},{"start":{"line":153,"column":37},"end":{"line":153,"column":46}}]},"20":{"loc":{"start":{"line":154,"column":16},"end":{"line":154,"column":48}},"type":"binary-expr","locations":[{"start":{"line":154,"column":16},"end":{"line":154,"column":35}},{"start":{"line":154,"column":39},"end":{"line":154,"column":48}}]},"21":{"loc":{"start":{"line":119,"column":15},"end":{"line":119,"column":null}},"type":"cond-expr","locations":[{"start":{"line":119,"column":43},"end":{"line":119,"column":null}},{"start":{"line":119,"column":43},"end":{"line":119,"column":null}}]},"22":{"loc":{"start":{"line":119,"column":15},"end":{"line":119,"column":43}},"type":"binary-expr","locations":[{"start":{"line":119,"column":15},"end":{"line":119,"column":null}},{"start":{"line":119,"column":15},"end":{"line":119,"column":43}}]},"23":{"loc":{"start":{"line":120,"column":7},"end":{"line":120,"column":null}},"type":"cond-expr","locations":[{"start":{"line":120,"column":14},"end":{"line":120,"column":null}},{"start":{"line":120,"column":14},"end":{"line":120,"column":null}}]},"24":{"loc":{"start":{"line":120,"column":7},"end":{"line":120,"column":14}},"type":"binary-expr","locations":[{"start":{"line":120,"column":7},"end":{"line":120,"column":null}},{"start":{"line":120,"column":7},"end":{"line":120,"column":14}}]},"25":{"loc":{"start":{"line":111,"column":38},"end":{"line":111,"column":null}},"type":"cond-expr","locations":[{"start":{"line":111,"column":62},"end":{"line":111,"column":null}},{"start":{"line":111,"column":62},"end":{"line":111,"column":null}}]},"26":{"loc":{"start":{"line":111,"column":38},"end":{"line":111,"column":62}},"type":"binary-expr","locations":[{"start":{"line":111,"column":38},"end":{"line":111,"column":null}},{"start":{"line":111,"column":38},"end":{"line":111,"column":62}}]},"27":{"loc":{"start":{"line":111,"column":62},"end":{"line":113,"column":null}},"type":"cond-expr","locations":[{"start":{"line":113,"column":54},"end":{"line":113,"column":null}},{"start":{"line":113,"column":54},"end":{"line":113,"column":null}}]},"28":{"loc":{"start":{"line":111,"column":62},"end":{"line":113,"column":54}},"type":"binary-expr","locations":[{"start":{"line":111,"column":62},"end":{"line":113,"column":null}},{"start":{"line":113,"column":34},"end":{"line":113,"column":54}}]},"29":{"loc":{"start":{"line":113,"column":54},"end":{"line":114,"column":null}},"type":"cond-expr","locations":[{"start":{"line":114,"column":53},"end":{"line":114,"column":null}},{"start":{"line":114,"column":53},"end":{"line":114,"column":null}}]},"30":{"loc":{"start":{"line":113,"column":54},"end":{"line":114,"column":53}},"type":"binary-expr","locations":[{"start":{"line":113,"column":54},"end":{"line":114,"column":null}},{"start":{"line":114,"column":40},"end":{"line":114,"column":53}}]}},"s":{"0":18,"1":18,"2":18,"3":18,"4":18,"5":18,"6":18,"7":18,"8":18,"9":18,"10":18,"11":18,"12":18,"13":5,"14":5,"15":2,"16":3,"17":3,"18":3,"19":3,"20":4,"21":3,"22":2,"23":2,"24":1,"25":2,"26":0,"27":18,"28":18,"29":18,"30":18,"31":18,"32":18,"33":18,"34":18,"35":6,"36":2,"37":4,"38":4,"39":4,"40":2,"41":2,"42":2,"43":0,"44":0,"45":2,"46":1,"47":1,"48":1,"49":0,"50":18,"51":18,"52":18},"f":{"0":18,"1":5,"2":4,"3":2,"4":18,"5":6},"b":{"0":[2,3],"1":[5,2],"2":[4,1],"3":[2,1],"4":[2,1],"5":[18,0],"6":[18,18],"7":[18,0],"8":[18,18],"9":[0,18],"10":[18,0],"11":[18,0],"12":[18,18],"13":[18,0],"14":[18,18],"15":[2,4],"16":[2,2],"17":[0,2],"18":[1,1],"19":[1,1],"20":[1,1],"21":[18,0],"22":[18,18],"23":[18,0],"24":[18,18],"25":[0,18],"26":[18,0],"27":[0,18],"28":[18,0],"29":[18,0],"30":[18,18]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/review-stage.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/review-stage.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":51}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":63}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":109}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":87}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":83}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"7":{"start":{"line":20,"column":53},"end":{"line":25,"column":2}},"8":{"start":{"line":32,"column":32},"end":{"line":125,"column":null}},"9":{"start":{"line":33,"column":30},"end":{"line":33,"column":66}},"10":{"start":{"line":36,"column":25},"end":{"line":36,"column":33}},"11":{"start":{"line":38,"column":25},"end":{"line":38,"column":34}},"12":{"start":{"line":39,"column":25},"end":{"line":39,"column":39}},"13":{"start":{"line":51,"column":8},"end":{"line":53,"column":10}},"14":{"start":{"line":55,"column":8},"end":{"line":123,"column":9}},"15":{"start":{"line":56,"column":12},"end":{"line":58,"column":13}},"16":{"start":{"line":57,"column":16},"end":{"line":57,"column":23}},"17":{"start":{"line":60,"column":27},"end":{"line":60,"column":70}},"18":{"start":{"line":62,"column":12},"end":{"line":65,"column":13}},"19":{"start":{"line":63,"column":16},"end":{"line":63,"column":71}},"20":{"start":{"line":64,"column":16},"end":{"line":64,"column":23}},"21":{"start":{"line":67,"column":12},"end":{"line":72,"column":13}},"22":{"start":{"line":68,"column":16},"end":{"line":70,"column":18}},"23":{"start":{"line":71,"column":16},"end":{"line":71,"column":23}},"24":{"start":{"line":74,"column":12},"end":{"line":76,"column":14}},"25":{"start":{"line":78,"column":12},"end":{"line":112,"column":13}},"26":{"start":{"line":79,"column":37},"end":{"line":80,"column":null}},"27":{"start":{"line":92,"column":40},"end":{"line":93,"column":null}},"28":{"start":{"line":93,"column":20},"end":{"line":93,"column":68}},"29":{"start":{"line":95,"column":37},"end":{"line":95,"column":65}},"30":{"start":{"line":97,"column":16},"end":{"line":106,"column":17}},"31":{"start":{"line":98,"column":20},"end":{"line":98,"column":68}},"32":{"start":{"line":99,"column":20},"end":{"line":101,"column":22}},"33":{"start":{"line":103,"column":20},"end":{"line":105,"column":22}},"34":{"start":{"line":108,"column":16},"end":{"line":111,"column":18}},"35":{"start":{"line":119,"column":12},"end":{"line":122,"column":14}},"36":{"start":{"line":32,"column":13},"end":{"line":32,"column":32}},"37":{"start":{"line":48,"column":10},"end":{"line":124,"column":null}},"38":{"start":{"line":32,"column":13},"end":{"line":125,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},"loc":{"start":{"line":39,"column":52},"end":{"line":40,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":48,"column":10},"end":{"line":48,"column":34}},"loc":{"start":{"line":49,"column":38},"end":{"line":124,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":92,"column":60},"end":{"line":92,"column":61}},"loc":{"start":{"line":93,"column":20},"end":{"line":93,"column":68}}}},"branchMap":{"0":{"loc":{"start":{"line":56,"column":12},"end":{"line":58,"column":13}},"type":"if","locations":[{"start":{"line":56,"column":12},"end":{"line":58,"column":13}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":62,"column":12},"end":{"line":65,"column":13}},"type":"if","locations":[{"start":{"line":62,"column":12},"end":{"line":65,"column":13}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":67,"column":12},"end":{"line":72,"column":13}},"type":"if","locations":[{"start":{"line":67,"column":12},"end":{"line":72,"column":13}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":97,"column":16},"end":{"line":106,"column":17}},"type":"if","locations":[{"start":{"line":97,"column":16},"end":{"line":106,"column":17}},{"start":{"line":102,"column":23},"end":{"line":106,"column":17}}]},"4":{"loc":{"start":{"line":49,"column":15},"end":{"line":49,"column":null}},"type":"cond-expr","locations":[{"start":{"line":49,"column":38},"end":{"line":49,"column":null}},{"start":{"line":49,"column":38},"end":{"line":49,"column":null}}]},"5":{"loc":{"start":{"line":49,"column":15},"end":{"line":49,"column":38}},"type":"binary-expr","locations":[{"start":{"line":49,"column":15},"end":{"line":49,"column":null}},{"start":{"line":49,"column":15},"end":{"line":49,"column":38}}]},"6":{"loc":{"start":{"line":50,"column":7},"end":{"line":50,"column":null}},"type":"cond-expr","locations":[{"start":{"line":50,"column":14},"end":{"line":50,"column":null}},{"start":{"line":50,"column":14},"end":{"line":50,"column":null}}]},"7":{"loc":{"start":{"line":50,"column":7},"end":{"line":50,"column":14}},"type":"binary-expr","locations":[{"start":{"line":50,"column":7},"end":{"line":50,"column":null}},{"start":{"line":50,"column":7},"end":{"line":50,"column":14}}]},"8":{"loc":{"start":{"line":36,"column":33},"end":{"line":36,"column":null}},"type":"cond-expr","locations":[{"start":{"line":36,"column":45},"end":{"line":36,"column":null}},{"start":{"line":36,"column":45},"end":{"line":36,"column":null}}]},"9":{"loc":{"start":{"line":36,"column":33},"end":{"line":36,"column":45}},"type":"binary-expr","locations":[{"start":{"line":36,"column":33},"end":{"line":36,"column":null}},{"start":{"line":36,"column":33},"end":{"line":36,"column":45}}]},"10":{"loc":{"start":{"line":36,"column":45},"end":{"line":38,"column":null}},"type":"cond-expr","locations":[{"start":{"line":38,"column":54},"end":{"line":38,"column":null}},{"start":{"line":38,"column":54},"end":{"line":38,"column":null}}]},"11":{"loc":{"start":{"line":36,"column":45},"end":{"line":38,"column":54}},"type":"binary-expr","locations":[{"start":{"line":36,"column":45},"end":{"line":38,"column":null}},{"start":{"line":38,"column":34},"end":{"line":38,"column":54}}]},"12":{"loc":{"start":{"line":38,"column":54},"end":{"line":39,"column":null}},"type":"cond-expr","locations":[{"start":{"line":39,"column":52},"end":{"line":39,"column":null}},{"start":{"line":39,"column":52},"end":{"line":39,"column":null}}]},"13":{"loc":{"start":{"line":38,"column":54},"end":{"line":39,"column":52}},"type":"binary-expr","locations":[{"start":{"line":38,"column":54},"end":{"line":39,"column":null}},{"start":{"line":39,"column":39},"end":{"line":39,"column":52}}]}},"s":{"0":18,"1":18,"2":18,"3":18,"4":18,"5":18,"6":18,"7":18,"8":18,"9":18,"10":18,"11":18,"12":18,"13":11,"14":11,"15":11,"16":3,"17":8,"18":8,"19":0,"20":0,"21":8,"22":1,"23":1,"24":7,"25":7,"26":7,"27":6,"28":7,"29":6,"30":6,"31":5,"32":1,"33":1,"34":4,"35":10,"36":18,"37":18,"38":18},"f":{"0":18,"1":11,"2":7},"b":{"0":[3,8],"1":[0,8],"2":[1,7],"3":[5,1],"4":[18,0],"5":[18,18],"6":[18,0],"7":[18,18],"8":[18,0],"9":[18,18],"10":[0,18],"11":[18,0],"12":[18,0],"13":[18,18]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/cluster-score-analytics.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/cluster-score-analytics.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"2":{"start":{"line":13,"column":0},"end":{"line":13,"column":33}},"3":{"start":{"line":14,"column":0},"end":{"line":14,"column":87}},"4":{"start":{"line":15,"column":0},"end":{"line":15,"column":90}},"5":{"start":{"line":16,"column":0},"end":{"line":16,"column":null}},"6":{"start":{"line":20,"column":0},"end":{"line":20,"column":null}},"7":{"start":{"line":24,"column":0},"end":{"line":24,"column":47}},"8":{"start":{"line":27,"column":7},"end":{"line":251,"column":null}},"9":{"start":{"line":30,"column":25},"end":{"line":30,"column":36}},"10":{"start":{"line":32,"column":25},"end":{"line":32,"column":40}},"11":{"start":{"line":33,"column":25},"end":{"line":33,"column":33}},"12":{"start":{"line":34,"column":25},"end":{"line":34,"column":35}},"13":{"start":{"line":40,"column":8},"end":{"line":40,"column":51}},"14":{"start":{"line":41,"column":25},"end":{"line":41,"column":54}},"15":{"start":{"line":42,"column":25},"end":{"line":42,"column":54}},"16":{"start":{"line":43,"column":29},"end":{"line":43,"column":62}},"17":{"start":{"line":44,"column":27},"end":{"line":44,"column":58}},"18":{"start":{"line":45,"column":27},"end":{"line":45,"column":58}},"19":{"start":{"line":47,"column":8},"end":{"line":47,"column":58}},"20":{"start":{"line":48,"column":8},"end":{"line":48,"column":68}},"21":{"start":{"line":50,"column":23},"end":{"line":60,"column":10}},"22":{"start":{"line":62,"column":8},"end":{"line":62,"column":45}},"23":{"start":{"line":68,"column":8},"end":{"line":68,"column":44}},"24":{"start":{"line":72,"column":21},"end":{"line":72,"column":54}},"25":{"start":{"line":73,"column":8},"end":{"line":76,"column":14}},"26":{"start":{"line":74,"column":12},"end":{"line":76,"column":14}},"27":{"start":{"line":77,"column":8},"end":{"line":77,"column":20}},"28":{"start":{"line":84,"column":24},"end":{"line":84,"column":46}},"29":{"start":{"line":87,"column":12},"end":{"line":89,"column":34}},"30":{"start":{"line":91,"column":12},"end":{"line":93,"column":34}},"31":{"start":{"line":95,"column":12},"end":{"line":97,"column":38}},"32":{"start":{"line":99,"column":8},"end":{"line":99,"column":68}},"33":{"start":{"line":101,"column":60},"end":{"line":117,"column":10}},"34":{"start":{"line":119,"column":8},"end":{"line":119,"column":54}},"35":{"start":{"line":123,"column":8},"end":{"line":123,"column":31}},"36":{"start":{"line":124,"column":8},"end":{"line":124,"column":44}},"37":{"start":{"line":130,"column":8},"end":{"line":130,"column":52}},"38":{"start":{"line":138,"column":8},"end":{"line":138,"column":43}},"39":{"start":{"line":140,"column":25},"end":{"line":140,"column":55}},"40":{"start":{"line":141,"column":30},"end":{"line":143,"column":10}},"41":{"start":{"line":145,"column":8},"end":{"line":200,"column":9}},"42":{"start":{"line":146,"column":12},"end":{"line":148,"column":13}},"43":{"start":{"line":147,"column":16},"end":{"line":147,"column":25}},"44":{"start":{"line":150,"column":41},"end":{"line":151,"column":null}},"45":{"start":{"line":151,"column":24},"end":{"line":151,"column":51}},"46":{"start":{"line":154,"column":12},"end":{"line":156,"column":13}},"47":{"start":{"line":155,"column":16},"end":{"line":155,"column":25}},"48":{"start":{"line":158,"column":27},"end":{"line":159,"column":null}},"49":{"start":{"line":159,"column":24},"end":{"line":159,"column":45}},"50":{"start":{"line":161,"column":29},"end":{"line":161,"column":70}},"51":{"start":{"line":162,"column":29},"end":{"line":162,"column":70}},"52":{"start":{"line":163,"column":33},"end":{"line":166,"column":35}},"53":{"start":{"line":164,"column":34},"end":{"line":164,"column":43}},"54":{"start":{"line":167,"column":35},"end":{"line":167,"column":62}},"55":{"start":{"line":169,"column":41},"end":{"line":171,"column":33}},"56":{"start":{"line":170,"column":31},"end":{"line":170,"column":70}},"57":{"start":{"line":171,"column":28},"end":{"line":171,"column":32}},"58":{"start":{"line":172,"column":39},"end":{"line":175,"column":null}},"59":{"start":{"line":174,"column":36},"end":{"line":174,"column":79}},"60":{"start":{"line":175,"column":33},"end":{"line":175,"column":43}},"61":{"start":{"line":177,"column":36},"end":{"line":179,"column":35}},"62":{"start":{"line":181,"column":31},"end":{"line":181,"column":62}},"63":{"start":{"line":182,"column":31},"end":{"line":182,"column":62}},"64":{"start":{"line":184,"column":12},"end":{"line":184,"column":62}},"65":{"start":{"line":185,"column":12},"end":{"line":185,"column":72}},"66":{"start":{"line":187,"column":27},"end":{"line":197,"column":14}},"67":{"start":{"line":199,"column":12},"end":{"line":199,"column":48}},"68":{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},"69":{"start":{"line":215,"column":12},"end":{"line":217,"column":14}},"70":{"start":{"line":219,"column":8},"end":{"line":223,"column":9}},"71":{"start":{"line":220,"column":12},"end":{"line":222,"column":14}},"72":{"start":{"line":235,"column":8},"end":{"line":239,"column":9}},"73":{"start":{"line":236,"column":12},"end":{"line":238,"column":14}},"74":{"start":{"line":240,"column":8},"end":{"line":244,"column":9}},"75":{"start":{"line":241,"column":12},"end":{"line":243,"column":14}},"76":{"start":{"line":245,"column":8},"end":{"line":249,"column":9}},"77":{"start":{"line":246,"column":12},"end":{"line":248,"column":14}},"78":{"start":{"line":27,"column":13},"end":{"line":27,"column":41}},"79":{"start":{"line":27,"column":13},"end":{"line":251,"column":null}}},"fnMap":{"0":{"name":"(anonymous_5)","decl":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"loc":{"start":{"line":34,"column":49},"end":{"line":35,"column":8}}},"1":{"name":"(anonymous_6)","decl":{"start":{"line":37,"column":4},"end":{"line":37,"column":9}},"loc":{"start":{"line":38,"column":51},"end":{"line":63,"column":5}}},"2":{"name":"(anonymous_7)","decl":{"start":{"line":65,"column":4},"end":{"line":65,"column":9}},"loc":{"start":{"line":66,"column":47},"end":{"line":69,"column":5}}},"3":{"name":"(anonymous_8)","decl":{"start":{"line":71,"column":4},"end":{"line":71,"column":9}},"loc":{"start":{"line":71,"column":28},"end":{"line":78,"column":5}}},"4":{"name":"(anonymous_9)","decl":{"start":{"line":80,"column":4},"end":{"line":80,"column":9}},"loc":{"start":{"line":82,"column":49},"end":{"line":120,"column":5}}},"5":{"name":"(anonymous_10)","decl":{"start":{"line":122,"column":4},"end":{"line":122,"column":9}},"loc":{"start":{"line":122,"column":27},"end":{"line":125,"column":5}}},"6":{"name":"(anonymous_11)","decl":{"start":{"line":127,"column":4},"end":{"line":127,"column":9}},"loc":{"start":{"line":128,"column":23},"end":{"line":131,"column":5}}},"7":{"name":"(anonymous_12)","decl":{"start":{"line":137,"column":4},"end":{"line":137,"column":9}},"loc":{"start":{"line":137,"column":51},"end":{"line":201,"column":5}}},"8":{"name":"(anonymous_13)","decl":{"start":{"line":151,"column":16},"end":{"line":151,"column":17}},"loc":{"start":{"line":151,"column":24},"end":{"line":151,"column":51}}},"9":{"name":"(anonymous_14)","decl":{"start":{"line":159,"column":16},"end":{"line":159,"column":17}},"loc":{"start":{"line":159,"column":24},"end":{"line":159,"column":45}}},"10":{"name":"(anonymous_15)","decl":{"start":{"line":164,"column":24},"end":{"line":164,"column":25}},"loc":{"start":{"line":164,"column":34},"end":{"line":164,"column":43}}},"11":{"name":"(anonymous_16)","decl":{"start":{"line":170,"column":24},"end":{"line":170,"column":25}},"loc":{"start":{"line":170,"column":31},"end":{"line":170,"column":70}}},"12":{"name":"(anonymous_17)","decl":{"start":{"line":171,"column":21},"end":{"line":171,"column":22}},"loc":{"start":{"line":171,"column":28},"end":{"line":171,"column":32}}},"13":{"name":"(anonymous_18)","decl":{"start":{"line":174,"column":28},"end":{"line":174,"column":29}},"loc":{"start":{"line":174,"column":36},"end":{"line":174,"column":79}}},"14":{"name":"(anonymous_19)","decl":{"start":{"line":175,"column":25},"end":{"line":175,"column":26}},"loc":{"start":{"line":175,"column":33},"end":{"line":175,"column":43}}},"15":{"name":"(anonymous_20)","decl":{"start":{"line":209,"column":12},"end":{"line":209,"column":17}},"loc":{"start":{"line":212,"column":20},"end":{"line":224,"column":5}}},"16":{"name":"(anonymous_21)","decl":{"start":{"line":231,"column":12},"end":{"line":231,"column":17}},"loc":{"start":{"line":233,"column":22},"end":{"line":250,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":73,"column":8},"end":{"line":76,"column":14}},"type":"if","locations":[{"start":{"line":73,"column":8},"end":{"line":76,"column":14}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":87,"column":12},"end":{"line":89,"column":34}},"type":"cond-expr","locations":[{"start":{"line":88,"column":18},"end":{"line":88,"column":45}},{"start":{"line":89,"column":18},"end":{"line":89,"column":34}}]},"2":{"loc":{"start":{"line":91,"column":12},"end":{"line":93,"column":34}},"type":"cond-expr","locations":[{"start":{"line":92,"column":18},"end":{"line":92,"column":45}},{"start":{"line":93,"column":18},"end":{"line":93,"column":34}}]},"3":{"loc":{"start":{"line":95,"column":12},"end":{"line":97,"column":38}},"type":"cond-expr","locations":[{"start":{"line":96,"column":18},"end":{"line":96,"column":49}},{"start":{"line":97,"column":18},"end":{"line":97,"column":38}}]},"4":{"loc":{"start":{"line":102,"column":16},"end":{"line":104,"column":20}},"type":"cond-expr","locations":[{"start":{"line":103,"column":18},"end":{"line":103,"column":58}},{"start":{"line":104,"column":18},"end":{"line":104,"column":20}}]},"5":{"loc":{"start":{"line":105,"column":16},"end":{"line":107,"column":20}},"type":"cond-expr","locations":[{"start":{"line":106,"column":18},"end":{"line":106,"column":60}},{"start":{"line":107,"column":18},"end":{"line":107,"column":20}}]},"6":{"loc":{"start":{"line":108,"column":16},"end":{"line":110,"column":20}},"type":"cond-expr","locations":[{"start":{"line":109,"column":18},"end":{"line":109,"column":46}},{"start":{"line":110,"column":18},"end":{"line":110,"column":20}}]},"7":{"loc":{"start":{"line":111,"column":16},"end":{"line":113,"column":20}},"type":"cond-expr","locations":[{"start":{"line":112,"column":18},"end":{"line":112,"column":46}},{"start":{"line":113,"column":18},"end":{"line":113,"column":20}}]},"8":{"loc":{"start":{"line":114,"column":16},"end":{"line":116,"column":20}},"type":"cond-expr","locations":[{"start":{"line":115,"column":18},"end":{"line":115,"column":54}},{"start":{"line":116,"column":18},"end":{"line":116,"column":20}}]},"9":{"loc":{"start":{"line":146,"column":12},"end":{"line":148,"column":13}},"type":"if","locations":[{"start":{"line":146,"column":12},"end":{"line":148,"column":13}},{"start":{},"end":{}}]},"10":{"loc":{"start":{"line":154,"column":12},"end":{"line":156,"column":13}},"type":"if","locations":[{"start":{"line":154,"column":12},"end":{"line":156,"column":13}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},"type":"if","locations":[{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":219,"column":8},"end":{"line":223,"column":9}},"type":"if","locations":[{"start":{"line":219,"column":8},"end":{"line":223,"column":9}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":219,"column":12},"end":{"line":219,"column":38}},"type":"binary-expr","locations":[{"start":{"line":219,"column":12},"end":{"line":219,"column":23}},{"start":{"line":219,"column":27},"end":{"line":219,"column":38}}]},"14":{"loc":{"start":{"line":235,"column":8},"end":{"line":239,"column":9}},"type":"if","locations":[{"start":{"line":235,"column":8},"end":{"line":239,"column":9}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":240,"column":8},"end":{"line":244,"column":9}},"type":"if","locations":[{"start":{"line":240,"column":8},"end":{"line":244,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":245,"column":8},"end":{"line":249,"column":9}},"type":"if","locations":[{"start":{"line":245,"column":8},"end":{"line":249,"column":9}},{"start":{},"end":{}}]},"17":{"loc":{"start":{"line":30,"column":36},"end":{"line":30,"column":null}},"type":"cond-expr","locations":[{"start":{"line":30,"column":71},"end":{"line":30,"column":null}},{"start":{"line":30,"column":71},"end":{"line":30,"column":null}}]},"18":{"loc":{"start":{"line":30,"column":36},"end":{"line":30,"column":71}},"type":"binary-expr","locations":[{"start":{"line":30,"column":36},"end":{"line":30,"column":null}},{"start":{"line":30,"column":36},"end":{"line":30,"column":71}}]},"19":{"loc":{"start":{"line":30,"column":71},"end":{"line":32,"column":null}},"type":"cond-expr","locations":[{"start":{"line":32,"column":66},"end":{"line":32,"column":null}},{"start":{"line":32,"column":66},"end":{"line":32,"column":null}}]},"20":{"loc":{"start":{"line":30,"column":71},"end":{"line":32,"column":66}},"type":"binary-expr","locations":[{"start":{"line":30,"column":71},"end":{"line":32,"column":null}},{"start":{"line":32,"column":40},"end":{"line":32,"column":66}}]},"21":{"loc":{"start":{"line":32,"column":66},"end":{"line":33,"column":null}},"type":"cond-expr","locations":[{"start":{"line":33,"column":45},"end":{"line":33,"column":null}},{"start":{"line":33,"column":45},"end":{"line":33,"column":null}}]},"22":{"loc":{"start":{"line":32,"column":66},"end":{"line":33,"column":45}},"type":"binary-expr","locations":[{"start":{"line":32,"column":66},"end":{"line":33,"column":null}},{"start":{"line":33,"column":33},"end":{"line":33,"column":45}}]},"23":{"loc":{"start":{"line":33,"column":45},"end":{"line":34,"column":null}},"type":"cond-expr","locations":[{"start":{"line":34,"column":49},"end":{"line":34,"column":null}},{"start":{"line":34,"column":49},"end":{"line":34,"column":null}}]},"24":{"loc":{"start":{"line":33,"column":45},"end":{"line":34,"column":49}},"type":"binary-expr","locations":[{"start":{"line":33,"column":45},"end":{"line":34,"column":null}},{"start":{"line":34,"column":35},"end":{"line":34,"column":49}}]}},"s":{"0":35,"1":35,"2":35,"3":35,"4":35,"5":35,"6":35,"7":35,"8":35,"9":35,"10":35,"11":35,"12":35,"13":13,"14":12,"15":12,"16":12,"17":12,"18":12,"19":12,"20":11,"21":9,"22":9,"23":1,"24":6,"25":6,"26":2,"27":4,"28":2,"29":2,"30":2,"31":2,"32":2,"33":1,"34":1,"35":1,"36":1,"37":6,"38":7,"39":6,"40":6,"41":6,"42":3,"43":0,"44":3,"45":3,"46":3,"47":1,"48":2,"49":3,"50":2,"51":2,"52":2,"53":3,"54":2,"55":2,"56":2,"57":2,"58":2,"59":3,"60":3,"61":2,"62":2,"63":2,"64":2,"65":2,"66":2,"67":2,"68":15,"69":1,"70":14,"71":2,"72":14,"73":0,"74":14,"75":0,"76":14,"77":1,"78":35,"79":35},"f":{"0":35,"1":13,"2":1,"3":6,"4":2,"5":1,"6":6,"7":7,"8":3,"9":3,"10":3,"11":2,"12":2,"13":3,"14":3,"15":15,"16":14},"b":{"0":[2,4],"1":[0,2],"2":[0,2],"3":[1,1],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,3],"10":[1,2],"11":[1,14],"12":[2,12],"13":[14,14],"14":[0,14],"15":[0,14],"16":[1,13],"17":[0,35],"18":[35,0],"19":[0,35],"20":[35,0],"21":[35,0],"22":[35,35],"23":[35,0],"24":[35,35]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/cycle.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/cycle.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"2":{"start":{"line":16,"column":0},"end":{"line":16,"column":54}},"3":{"start":{"line":17,"column":0},"end":{"line":17,"column":null}},"4":{"start":{"line":21,"column":0},"end":{"line":21,"column":60}},"5":{"start":{"line":22,"column":0},"end":{"line":22,"column":82}},"6":{"start":{"line":23,"column":0},"end":{"line":23,"column":56}},"7":{"start":{"line":24,"column":0},"end":{"line":24,"column":null}},"8":{"start":{"line":28,"column":0},"end":{"line":28,"column":77}},"9":{"start":{"line":29,"column":0},"end":{"line":29,"column":null}},"10":{"start":{"line":33,"column":0},"end":{"line":33,"column":null}},"11":{"start":{"line":37,"column":0},"end":{"line":37,"column":null}},"12":{"start":{"line":43,"column":7},"end":{"line":313,"column":null}},"13":{"start":{"line":46,"column":25},"end":{"line":46,"column":33}},"14":{"start":{"line":48,"column":25},"end":{"line":48,"column":39}},"15":{"start":{"line":50,"column":25},"end":{"line":50,"column":34}},"16":{"start":{"line":51,"column":25},"end":{"line":51,"column":39}},"17":{"start":{"line":52,"column":25},"end":{"line":52,"column":33}},"18":{"start":{"line":54,"column":25},"end":{"line":54,"column":43}},"19":{"start":{"line":58,"column":22},"end":{"line":72,"column":10}},"20":{"start":{"line":74,"column":24},"end":{"line":74,"column":55}},"21":{"start":{"line":75,"column":8},"end":{"line":75,"column":41}},"22":{"start":{"line":79,"column":8},"end":{"line":79,"column":41}},"23":{"start":{"line":83,"column":22},"end":{"line":83,"column":52}},"24":{"start":{"line":84,"column":8},"end":{"line":85,"column":78}},"25":{"start":{"line":85,"column":12},"end":{"line":85,"column":78}},"26":{"start":{"line":86,"column":8},"end":{"line":86,"column":21}},"27":{"start":{"line":90,"column":22},"end":{"line":90,"column":44}},"28":{"start":{"line":92,"column":44},"end":{"line":114,"column":10}},"29":{"start":{"line":117,"column":32},"end":{"line":117,"column":77}},"30":{"start":{"line":119,"column":8},"end":{"line":126,"column":9}},"31":{"start":{"line":120,"column":12},"end":{"line":125,"column":14}},"32":{"start":{"line":128,"column":24},"end":{"line":128,"column":65}},"33":{"start":{"line":130,"column":8},"end":{"line":177,"column":9}},"34":{"start":{"line":132,"column":28},"end":{"line":132,"column":64}},"35":{"start":{"line":133,"column":38},"end":{"line":138,"column":null}},"36":{"start":{"line":134,"column":16},"end":{"line":138,"column":35}},"37":{"start":{"line":140,"column":41},"end":{"line":140,"column":69}},"38":{"start":{"line":142,"column":12},"end":{"line":161,"column":13}},"39":{"start":{"line":153,"column":16},"end":{"line":160,"column":18}},"40":{"start":{"line":164,"column":12},"end":{"line":166,"column":13}},"41":{"start":{"line":165,"column":16},"end":{"line":165,"column":45}},"42":{"start":{"line":168,"column":12},"end":{"line":176,"column":13}},"43":{"start":{"line":169,"column":16},"end":{"line":175,"column":18}},"44":{"start":{"line":179,"column":8},"end":{"line":179,"column":23}},"45":{"start":{"line":183,"column":22},"end":{"line":183,"column":44}},"46":{"start":{"line":185,"column":8},"end":{"line":185,"column":41}},"47":{"start":{"line":203,"column":22},"end":{"line":203,"column":49}},"48":{"start":{"line":204,"column":29},"end":{"line":204,"column":40}},"49":{"start":{"line":207,"column":35},"end":{"line":207,"column":72}},"50":{"start":{"line":209,"column":8},"end":{"line":213,"column":9}},"51":{"start":{"line":210,"column":12},"end":{"line":212,"column":14}},"52":{"start":{"line":216,"column":8},"end":{"line":234,"column":11}},"53":{"start":{"line":218,"column":12},"end":{"line":221,"column":15}},"54":{"start":{"line":224,"column":28},"end":{"line":231,"column":14}},"55":{"start":{"line":233,"column":12},"end":{"line":233,"column":52}},"56":{"start":{"line":237,"column":8},"end":{"line":247,"column":10}},"57":{"start":{"line":254,"column":8},"end":{"line":254,"column":56}},"58":{"start":{"line":263,"column":22},"end":{"line":263,"column":49}},"59":{"start":{"line":266,"column":24},"end":{"line":266,"column":65}},"60":{"start":{"line":269,"column":34},"end":{"line":272,"column":null}},"61":{"start":{"line":271,"column":16},"end":{"line":272,"column":51}},"62":{"start":{"line":274,"column":37},"end":{"line":274,"column":65}},"63":{"start":{"line":277,"column":8},"end":{"line":292,"column":9}},"64":{"start":{"line":278,"column":12},"end":{"line":284,"column":14}},"65":{"start":{"line":285,"column":15},"end":{"line":292,"column":9}},"66":{"start":{"line":286,"column":12},"end":{"line":291,"column":14}},"67":{"start":{"line":304,"column":8},"end":{"line":304,"column":36}},"68":{"start":{"line":305,"column":8},"end":{"line":311,"column":10}},"69":{"start":{"line":43,"column":13},"end":{"line":43,"column":25}},"70":{"start":{"line":43,"column":13},"end":{"line":313,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":44,"column":4},"end":{"line":44,"column":null}},"loc":{"start":{"line":54,"column":72},"end":{"line":55,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":57,"column":4},"end":{"line":57,"column":9}},"loc":{"start":{"line":57,"column":44},"end":{"line":76,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":78,"column":4},"end":{"line":78,"column":9}},"loc":{"start":{"line":78,"column":40},"end":{"line":80,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":82,"column":4},"end":{"line":82,"column":9}},"loc":{"start":{"line":82,"column":28},"end":{"line":87,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":89,"column":4},"end":{"line":89,"column":9}},"loc":{"start":{"line":89,"column":54},"end":{"line":180,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":133,"column":53},"end":{"line":133,"column":54}},"loc":{"start":{"line":134,"column":16},"end":{"line":138,"column":35}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":182,"column":4},"end":{"line":182,"column":9}},"loc":{"start":{"line":182,"column":27},"end":{"line":186,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":196,"column":4},"end":{"line":196,"column":9}},"loc":{"start":{"line":201,"column":23},"end":{"line":248,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":216,"column":39},"end":{"line":216,"column":44}},"loc":{"start":{"line":216,"column":52},"end":{"line":234,"column":9}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":253,"column":4},"end":{"line":253,"column":9}},"loc":{"start":{"line":253,"column":41},"end":{"line":255,"column":5}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":262,"column":4},"end":{"line":262,"column":9}},"loc":{"start":{"line":262,"column":39},"end":{"line":293,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":270,"column":12},"end":{"line":270,"column":13}},"loc":{"start":{"line":271,"column":16},"end":{"line":272,"column":51}}},"12":{"name":"(anonymous_16)","decl":{"start":{"line":299,"column":4},"end":{"line":299,"column":9}},"loc":{"start":{"line":302,"column":52},"end":{"line":312,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":62,"column":19},"end":{"line":62,"column":50}},"type":"binary-expr","locations":[{"start":{"line":62,"column":19},"end":{"line":62,"column":32}},{"start":{"line":62,"column":36},"end":{"line":62,"column":50}}]},"1":{"loc":{"start":{"line":64,"column":16},"end":{"line":65,"column":57}},"type":"binary-expr","locations":[{"start":{"line":64,"column":16},"end":{"line":64,"column":47}},{"start":{"line":65,"column":16},"end":{"line":65,"column":57}}]},"2":{"loc":{"start":{"line":66,"column":22},"end":{"line":66,"column":46}},"type":"binary-expr","locations":[{"start":{"line":66,"column":22},"end":{"line":66,"column":38}},{"start":{"line":66,"column":42},"end":{"line":66,"column":46}}]},"3":{"loc":{"start":{"line":84,"column":8},"end":{"line":85,"column":78}},"type":"if","locations":[{"start":{"line":84,"column":8},"end":{"line":85,"column":78}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":93,"column":16},"end":{"line":93,"column":71}},"type":"cond-expr","locations":[{"start":{"line":93,"column":44},"end":{"line":93,"column":66}},{"start":{"line":93,"column":69},"end":{"line":93,"column":71}}]},"5":{"loc":{"start":{"line":94,"column":16},"end":{"line":96,"column":20}},"type":"cond-expr","locations":[{"start":{"line":95,"column":18},"end":{"line":95,"column":52}},{"start":{"line":96,"column":18},"end":{"line":96,"column":20}}]},"6":{"loc":{"start":{"line":97,"column":16},"end":{"line":97,"column":68}},"type":"cond-expr","locations":[{"start":{"line":97,"column":43},"end":{"line":97,"column":63}},{"start":{"line":97,"column":66},"end":{"line":97,"column":68}}]},"7":{"loc":{"start":{"line":98,"column":16},"end":{"line":100,"column":20}},"type":"cond-expr","locations":[{"start":{"line":99,"column":18},"end":{"line":99,"column":46}},{"start":{"line":100,"column":18},"end":{"line":100,"column":20}}]},"8":{"loc":{"start":{"line":101,"column":16},"end":{"line":103,"column":20}},"type":"cond-expr","locations":[{"start":{"line":102,"column":18},"end":{"line":102,"column":48}},{"start":{"line":103,"column":18},"end":{"line":103,"column":20}}]},"9":{"loc":{"start":{"line":104,"column":16},"end":{"line":106,"column":20}},"type":"cond-expr","locations":[{"start":{"line":105,"column":18},"end":{"line":105,"column":58}},{"start":{"line":106,"column":18},"end":{"line":106,"column":20}}]},"10":{"loc":{"start":{"line":107,"column":16},"end":{"line":109,"column":20}},"type":"cond-expr","locations":[{"start":{"line":108,"column":18},"end":{"line":108,"column":62}},{"start":{"line":109,"column":18},"end":{"line":109,"column":20}}]},"11":{"loc":{"start":{"line":110,"column":16},"end":{"line":112,"column":20}},"type":"cond-expr","locations":[{"start":{"line":111,"column":18},"end":{"line":111,"column":62}},{"start":{"line":112,"column":18},"end":{"line":112,"column":20}}]},"12":{"loc":{"start":{"line":113,"column":16},"end":{"line":113,"column":77}},"type":"cond-expr","locations":[{"start":{"line":113,"column":46},"end":{"line":113,"column":72}},{"start":{"line":113,"column":75},"end":{"line":113,"column":77}}]},"13":{"loc":{"start":{"line":119,"column":8},"end":{"line":126,"column":9}},"type":"if","locations":[{"start":{"line":119,"column":8},"end":{"line":126,"column":9}},{"start":{},"end":{}}]},"14":{"loc":{"start":{"line":119,"column":12},"end":{"line":119,"column":53}},"type":"binary-expr","locations":[{"start":{"line":119,"column":12},"end":{"line":119,"column":36}},{"start":{"line":119,"column":40},"end":{"line":119,"column":53}}]},"15":{"loc":{"start":{"line":130,"column":8},"end":{"line":177,"column":9}},"type":"if","locations":[{"start":{"line":130,"column":8},"end":{"line":177,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":130,"column":12},"end":{"line":130,"column":70}},"type":"binary-expr","locations":[{"start":{"line":130,"column":12},"end":{"line":130,"column":23}},{"start":{"line":130,"column":28},"end":{"line":130,"column":70}}]},"17":{"loc":{"start":{"line":142,"column":12},"end":{"line":161,"column":13}},"type":"if","locations":[{"start":{"line":142,"column":12},"end":{"line":161,"column":13}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":143,"column":16},"end":{"line":151,"column":39}},"type":"binary-expr","locations":[{"start":{"line":143,"column":16},"end":{"line":143,"column":36}},{"start":{"line":144,"column":16},"end":{"line":144,"column":27}},{"start":{"line":145,"column":16},"end":{"line":151,"column":39}}]},"19":{"loc":{"start":{"line":164,"column":12},"end":{"line":166,"column":13}},"type":"if","locations":[{"start":{"line":164,"column":12},"end":{"line":166,"column":13}},{"start":{},"end":{}}]},"20":{"loc":{"start":{"line":164,"column":16},"end":{"line":164,"column":66}},"type":"binary-expr","locations":[{"start":{"line":164,"column":16},"end":{"line":164,"column":27}},{"start":{"line":164,"column":31},"end":{"line":164,"column":66}}]},"21":{"loc":{"start":{"line":168,"column":12},"end":{"line":176,"column":13}},"type":"if","locations":[{"start":{"line":168,"column":12},"end":{"line":176,"column":13}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":168,"column":16},"end":{"line":168,"column":67}},"type":"binary-expr","locations":[{"start":{"line":168,"column":16},"end":{"line":168,"column":27}},{"start":{"line":168,"column":31},"end":{"line":168,"column":67}}]},"23":{"loc":{"start":{"line":199,"column":8},"end":{"line":199,"column":41}},"type":"default-arg","locations":[{"start":{"line":199,"column":26},"end":{"line":199,"column":41}}]},"24":{"loc":{"start":{"line":200,"column":8},"end":{"line":200,"column":52}},"type":"default-arg","locations":[{"start":{"line":200,"column":35},"end":{"line":200,"column":52}}]},"25":{"loc":{"start":{"line":209,"column":8},"end":{"line":213,"column":9}},"type":"if","locations":[{"start":{"line":209,"column":8},"end":{"line":213,"column":9}},{"start":{},"end":{}}]},"26":{"loc":{"start":{"line":228,"column":29},"end":{"line":228,"column":44}},"type":"binary-expr","locations":[{"start":{"line":228,"column":29},"end":{"line":228,"column":36}},{"start":{"line":228,"column":40},"end":{"line":228,"column":44}}]},"27":{"loc":{"start":{"line":271,"column":16},"end":{"line":272,"column":51}},"type":"binary-expr","locations":[{"start":{"line":271,"column":16},"end":{"line":271,"column":55}},{"start":{"line":272,"column":16},"end":{"line":272,"column":51}}]},"28":{"loc":{"start":{"line":277,"column":8},"end":{"line":292,"column":9}},"type":"if","locations":[{"start":{"line":277,"column":8},"end":{"line":292,"column":9}},{"start":{"line":285,"column":15},"end":{"line":292,"column":9}}]},"29":{"loc":{"start":{"line":277,"column":12},"end":{"line":277,"column":55}},"type":"binary-expr","locations":[{"start":{"line":277,"column":12},"end":{"line":277,"column":33}},{"start":{"line":277,"column":37},"end":{"line":277,"column":55}}]},"30":{"loc":{"start":{"line":285,"column":15},"end":{"line":292,"column":9}},"type":"if","locations":[{"start":{"line":285,"column":15},"end":{"line":292,"column":9}},{"start":{},"end":{}}]},"31":{"loc":{"start":{"line":285,"column":19},"end":{"line":285,"column":61}},"type":"binary-expr","locations":[{"start":{"line":285,"column":19},"end":{"line":285,"column":39}},{"start":{"line":285,"column":43},"end":{"line":285,"column":61}}]},"32":{"loc":{"start":{"line":301,"column":8},"end":{"line":301,"column":41}},"type":"default-arg","locations":[{"start":{"line":301,"column":26},"end":{"line":301,"column":41}}]},"33":{"loc":{"start":{"line":302,"column":8},"end":{"line":302,"column":52}},"type":"default-arg","locations":[{"start":{"line":302,"column":35},"end":{"line":302,"column":52}}]},"34":{"loc":{"start":{"line":46,"column":33},"end":{"line":46,"column":null}},"type":"cond-expr","locations":[{"start":{"line":46,"column":52},"end":{"line":46,"column":null}},{"start":{"line":46,"column":52},"end":{"line":46,"column":null}}]},"35":{"loc":{"start":{"line":46,"column":33},"end":{"line":46,"column":52}},"type":"binary-expr","locations":[{"start":{"line":46,"column":33},"end":{"line":46,"column":null}},{"start":{"line":46,"column":33},"end":{"line":46,"column":52}}]},"36":{"loc":{"start":{"line":46,"column":52},"end":{"line":48,"column":null}},"type":"cond-expr","locations":[{"start":{"line":48,"column":70},"end":{"line":48,"column":null}},{"start":{"line":48,"column":70},"end":{"line":48,"column":null}}]},"37":{"loc":{"start":{"line":46,"column":52},"end":{"line":48,"column":70}},"type":"binary-expr","locations":[{"start":{"line":46,"column":52},"end":{"line":48,"column":null}},{"start":{"line":48,"column":39},"end":{"line":48,"column":70}}]},"38":{"loc":{"start":{"line":48,"column":70},"end":{"line":50,"column":null}},"type":"cond-expr","locations":[{"start":{"line":50,"column":54},"end":{"line":50,"column":null}},{"start":{"line":50,"column":54},"end":{"line":50,"column":null}}]},"39":{"loc":{"start":{"line":48,"column":70},"end":{"line":50,"column":54}},"type":"binary-expr","locations":[{"start":{"line":48,"column":70},"end":{"line":50,"column":null}},{"start":{"line":50,"column":34},"end":{"line":50,"column":54}}]},"40":{"loc":{"start":{"line":50,"column":54},"end":{"line":51,"column":null}},"type":"cond-expr","locations":[{"start":{"line":51,"column":52},"end":{"line":51,"column":null}},{"start":{"line":51,"column":52},"end":{"line":51,"column":null}}]},"41":{"loc":{"start":{"line":50,"column":54},"end":{"line":51,"column":52}},"type":"binary-expr","locations":[{"start":{"line":50,"column":54},"end":{"line":51,"column":null}},{"start":{"line":51,"column":39},"end":{"line":51,"column":52}}]},"42":{"loc":{"start":{"line":51,"column":52},"end":{"line":52,"column":null}},"type":"cond-expr","locations":[{"start":{"line":52,"column":46},"end":{"line":52,"column":null}},{"start":{"line":52,"column":46},"end":{"line":52,"column":null}}]},"43":{"loc":{"start":{"line":51,"column":52},"end":{"line":52,"column":46}},"type":"binary-expr","locations":[{"start":{"line":51,"column":52},"end":{"line":52,"column":null}},{"start":{"line":52,"column":33},"end":{"line":52,"column":46}}]},"44":{"loc":{"start":{"line":52,"column":46},"end":{"line":54,"column":null}},"type":"cond-expr","locations":[{"start":{"line":54,"column":72},"end":{"line":54,"column":null}},{"start":{"line":54,"column":72},"end":{"line":54,"column":null}}]},"45":{"loc":{"start":{"line":52,"column":46},"end":{"line":54,"column":72}},"type":"binary-expr","locations":[{"start":{"line":52,"column":46},"end":{"line":54,"column":null}},{"start":{"line":54,"column":43},"end":{"line":54,"column":72}}]}},"s":{"0":38,"1":38,"2":38,"3":38,"4":38,"5":38,"6":38,"7":38,"8":38,"9":38,"10":38,"11":38,"12":38,"13":37,"14":37,"15":37,"16":37,"17":37,"18":37,"19":60,"20":60,"21":60,"22":7,"23":151,"24":151,"25":9,"26":142,"27":5,"28":4,"29":4,"30":4,"31":0,"32":4,"33":4,"34":3,"35":3,"36":1,"37":3,"38":3,"39":1,"40":2,"41":0,"42":2,"43":2,"44":3,"45":2,"46":1,"47":16,"48":15,"49":15,"50":15,"51":2,"52":13,"53":13,"54":13,"55":13,"56":13,"57":2,"58":8,"59":6,"60":6,"61":5,"62":6,"63":6,"64":4,"65":2,"66":1,"67":2,"68":2,"69":38,"70":38},"f":{"0":37,"1":60,"2":7,"3":151,"4":5,"5":1,"6":2,"7":16,"8":13,"9":2,"10":8,"11":5,"12":2},"b":{"0":[60,60],"1":[60,60],"2":[60,60],"3":[9,142],"4":[0,4],"5":[1,3],"6":[0,4],"7":[0,4],"8":[0,4],"9":[0,4],"10":[0,4],"11":[0,4],"12":[0,4],"13":[0,4],"14":[4,0],"15":[3,1],"16":[4,3],"17":[1,2],"18":[3,1,1],"19":[0,2],"20":[2,2],"21":[2,0],"22":[2,2],"23":[7],"24":[7],"25":[2,13],"26":[13,13],"27":[5,5],"28":[4,2],"29":[6,5],"30":[1,1],"31":[2,1],"32":[2],"33":[2],"34":[0,38],"35":[38,0],"36":[0,38],"37":[38,0],"38":[0,38],"39":[38,0],"40":[38,0],"41":[38,38],"42":[38,0],"43":[38,38],"44":[0,38],"45":[38,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/review-scheduler.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/review-scheduler.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":52}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":56}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":70}},"4":{"start":{"line":10,"column":0},"end":{"line":10,"column":75}},"5":{"start":{"line":11,"column":0},"end":{"line":11,"column":47}},"6":{"start":{"line":12,"column":0},"end":{"line":12,"column":49}},"7":{"start":{"line":18,"column":20},"end":{"line":24,"column":2}},"8":{"start":{"line":35,"column":35},"end":{"line":152,"column":null}},"9":{"start":{"line":36,"column":30},"end":{"line":36,"column":69}},"10":{"start":{"line":39,"column":25},"end":{"line":39,"column":40}},"11":{"start":{"line":40,"column":25},"end":{"line":40,"column":39}},"12":{"start":{"line":49,"column":8},"end":{"line":49,"column":65}},"13":{"start":{"line":51,"column":8},"end":{"line":105,"column":9}},"14":{"start":{"line":53,"column":27},"end":{"line":55,"column":14}},"15":{"start":{"line":57,"column":12},"end":{"line":100,"column":13}},"16":{"start":{"line":58,"column":16},"end":{"line":60,"column":17}},"17":{"start":{"line":59,"column":20},"end":{"line":59,"column":29}},"18":{"start":{"line":62,"column":28},"end":{"line":62,"column":38}},"19":{"start":{"line":63,"column":33},"end":{"line":63,"column":65}},"20":{"start":{"line":66,"column":16},"end":{"line":99,"column":17}},"21":{"start":{"line":68,"column":36},"end":{"line":73,"column":null}},"22":{"start":{"line":76,"column":20},"end":{"line":78,"column":22}},"23":{"start":{"line":81,"column":20},"end":{"line":98,"column":21}},"24":{"start":{"line":82,"column":24},"end":{"line":97,"column":25}},"25":{"start":{"line":83,"column":28},"end":{"line":89,"column":30}},"26":{"start":{"line":90,"column":28},"end":{"line":92,"column":30}},"27":{"start":{"line":94,"column":28},"end":{"line":96,"column":30}},"28":{"start":{"line":102,"column":12},"end":{"line":104,"column":14}},"29":{"start":{"line":114,"column":8},"end":{"line":114,"column":64}},"30":{"start":{"line":116,"column":8},"end":{"line":150,"column":9}},"31":{"start":{"line":117,"column":27},"end":{"line":120,"column":14}},"32":{"start":{"line":122,"column":24},"end":{"line":122,"column":34}},"33":{"start":{"line":124,"column":12},"end":{"line":145,"column":13}},"34":{"start":{"line":126,"column":20},"end":{"line":126,"column":66}},"35":{"start":{"line":128,"column":20},"end":{"line":129,"column":58}},"36":{"start":{"line":131,"column":16},"end":{"line":133,"column":17}},"37":{"start":{"line":132,"column":20},"end":{"line":132,"column":29}},"38":{"start":{"line":135,"column":16},"end":{"line":144,"column":17}},"39":{"start":{"line":136,"column":20},"end":{"line":136,"column":67}},"40":{"start":{"line":137,"column":20},"end":{"line":139,"column":22}},"41":{"start":{"line":141,"column":20},"end":{"line":143,"column":22}},"42":{"start":{"line":147,"column":12},"end":{"line":149,"column":14}},"43":{"start":{"line":35,"column":13},"end":{"line":35,"column":35}},"44":{"start":{"line":48,"column":10},"end":{"line":106,"column":null}},"45":{"start":{"line":113,"column":10},"end":{"line":151,"column":null}},"46":{"start":{"line":35,"column":13},"end":{"line":152,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":38,"column":4},"end":{"line":38,"column":null}},"loc":{"start":{"line":40,"column":51},"end":{"line":41,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":48,"column":10},"end":{"line":48,"column":30}},"loc":{"start":{"line":48,"column":30},"end":{"line":106,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":113,"column":10},"end":{"line":113,"column":29}},"loc":{"start":{"line":113,"column":29},"end":{"line":151,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":58,"column":16},"end":{"line":60,"column":17}},"type":"if","locations":[{"start":{"line":58,"column":16},"end":{"line":60,"column":17}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":66,"column":16},"end":{"line":99,"column":17}},"type":"if","locations":[{"start":{"line":66,"column":16},"end":{"line":99,"column":17}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":126,"column":20},"end":{"line":126,"column":66}},"type":"binary-expr","locations":[{"start":{"line":126,"column":20},"end":{"line":126,"column":33}},{"start":{"line":126,"column":37},"end":{"line":126,"column":66}}]},"3":{"loc":{"start":{"line":128,"column":20},"end":{"line":129,"column":58}},"type":"binary-expr","locations":[{"start":{"line":128,"column":20},"end":{"line":128,"column":42}},{"start":{"line":129,"column":20},"end":{"line":129,"column":58}}]},"4":{"loc":{"start":{"line":131,"column":16},"end":{"line":133,"column":17}},"type":"if","locations":[{"start":{"line":131,"column":16},"end":{"line":133,"column":17}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":131,"column":20},"end":{"line":131,"column":61}},"type":"binary-expr","locations":[{"start":{"line":131,"column":20},"end":{"line":131,"column":34}},{"start":{"line":131,"column":38},"end":{"line":131,"column":61}}]},"6":{"loc":{"start":{"line":48,"column":34},"end":{"line":48,"column":null}},"type":"cond-expr","locations":[{"start":{"line":48,"column":41},"end":{"line":48,"column":null}},{"start":{"line":48,"column":41},"end":{"line":48,"column":null}}]},"7":{"loc":{"start":{"line":48,"column":34},"end":{"line":48,"column":41}},"type":"binary-expr","locations":[{"start":{"line":48,"column":34},"end":{"line":48,"column":null}},{"start":{"line":48,"column":34},"end":{"line":48,"column":41}}]},"8":{"loc":{"start":{"line":113,"column":33},"end":{"line":113,"column":null}},"type":"cond-expr","locations":[{"start":{"line":113,"column":40},"end":{"line":113,"column":null}},{"start":{"line":113,"column":40},"end":{"line":113,"column":null}}]},"9":{"loc":{"start":{"line":113,"column":33},"end":{"line":113,"column":40}},"type":"binary-expr","locations":[{"start":{"line":113,"column":33},"end":{"line":113,"column":null}},{"start":{"line":113,"column":33},"end":{"line":113,"column":40}}]},"10":{"loc":{"start":{"line":39,"column":40},"end":{"line":39,"column":null}},"type":"cond-expr","locations":[{"start":{"line":39,"column":53},"end":{"line":39,"column":null}},{"start":{"line":39,"column":53},"end":{"line":39,"column":null}}]},"11":{"loc":{"start":{"line":39,"column":40},"end":{"line":39,"column":53}},"type":"binary-expr","locations":[{"start":{"line":39,"column":40},"end":{"line":39,"column":null}},{"start":{"line":39,"column":40},"end":{"line":39,"column":53}}]},"12":{"loc":{"start":{"line":39,"column":53},"end":{"line":40,"column":null}},"type":"cond-expr","locations":[{"start":{"line":40,"column":51},"end":{"line":40,"column":null}},{"start":{"line":40,"column":51},"end":{"line":40,"column":null}}]},"13":{"loc":{"start":{"line":39,"column":53},"end":{"line":40,"column":51}},"type":"binary-expr","locations":[{"start":{"line":39,"column":53},"end":{"line":40,"column":null}},{"start":{"line":40,"column":39},"end":{"line":40,"column":51}}]}},"s":{"0":18,"1":18,"2":18,"3":18,"4":18,"5":18,"6":18,"7":18,"8":18,"9":18,"10":18,"11":18,"12":4,"13":4,"14":4,"15":4,"16":4,"17":1,"18":3,"19":3,"20":3,"21":2,"22":2,"23":2,"24":1,"25":1,"26":1,"27":0,"28":0,"29":2,"30":2,"31":2,"32":2,"33":2,"34":2,"35":2,"36":2,"37":1,"38":1,"39":1,"40":1,"41":0,"42":0,"43":18,"44":18,"45":18,"46":18},"f":{"0":18,"1":4,"2":2},"b":{"0":[1,3],"1":[2,1],"2":[2,2],"3":[2,1],"4":[1,1],"5":[2,1],"6":[18,0],"7":[18,18],"8":[18,0],"9":[18,18],"10":[18,0],"11":[18,18],"12":[18,0],"13":[18,18]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/review.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/review.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":24,"column":0},"end":{"line":24,"column":null}},"2":{"start":{"line":31,"column":0},"end":{"line":31,"column":54}},"3":{"start":{"line":33,"column":0},"end":{"line":33,"column":97}},"4":{"start":{"line":34,"column":0},"end":{"line":34,"column":110}},"5":{"start":{"line":35,"column":0},"end":{"line":35,"column":60}},"6":{"start":{"line":36,"column":0},"end":{"line":36,"column":58}},"7":{"start":{"line":38,"column":0},"end":{"line":38,"column":71}},"8":{"start":{"line":39,"column":0},"end":{"line":39,"column":62}},"9":{"start":{"line":40,"column":0},"end":{"line":40,"column":66}},"10":{"start":{"line":41,"column":0},"end":{"line":41,"column":92}},"11":{"start":{"line":42,"column":0},"end":{"line":42,"column":84}},"12":{"start":{"line":43,"column":0},"end":{"line":43,"column":58}},"13":{"start":{"line":44,"column":0},"end":{"line":44,"column":62}},"14":{"start":{"line":45,"column":0},"end":{"line":45,"column":null}},"15":{"start":{"line":49,"column":0},"end":{"line":49,"column":null}},"16":{"start":{"line":53,"column":0},"end":{"line":53,"column":89}},"17":{"start":{"line":54,"column":0},"end":{"line":54,"column":79}},"18":{"start":{"line":55,"column":0},"end":{"line":55,"column":null}},"19":{"start":{"line":59,"column":0},"end":{"line":59,"column":null}},"20":{"start":{"line":63,"column":0},"end":{"line":63,"column":null}},"21":{"start":{"line":67,"column":0},"end":{"line":67,"column":null}},"22":{"start":{"line":71,"column":0},"end":{"line":71,"column":null}},"23":{"start":{"line":75,"column":0},"end":{"line":75,"column":null}},"24":{"start":{"line":79,"column":0},"end":{"line":79,"column":null}},"25":{"start":{"line":83,"column":0},"end":{"line":83,"column":null}},"26":{"start":{"line":87,"column":0},"end":{"line":87,"column":47}},"27":{"start":{"line":90,"column":7},"end":{"line":1025,"column":null}},"28":{"start":{"line":93,"column":25},"end":{"line":93,"column":34}},"29":{"start":{"line":95,"column":25},"end":{"line":95,"column":36}},"30":{"start":{"line":97,"column":25},"end":{"line":97,"column":44}},"31":{"start":{"line":99,"column":25},"end":{"line":99,"column":34}},"32":{"start":{"line":101,"column":25},"end":{"line":101,"column":38}},"33":{"start":{"line":103,"column":25},"end":{"line":103,"column":36}},"34":{"start":{"line":105,"column":25},"end":{"line":105,"column":40}},"35":{"start":{"line":107,"column":25},"end":{"line":107,"column":39}},"36":{"start":{"line":108,"column":25},"end":{"line":108,"column":33}},"37":{"start":{"line":109,"column":25},"end":{"line":109,"column":44}},"38":{"start":{"line":110,"column":25},"end":{"line":110,"column":38}},"39":{"start":{"line":111,"column":25},"end":{"line":111,"column":33}},"40":{"start":{"line":112,"column":25},"end":{"line":112,"column":39}},"41":{"start":{"line":116,"column":8},"end":{"line":118,"column":9}},"42":{"start":{"line":117,"column":12},"end":{"line":117,"column":55}},"43":{"start":{"line":120,"column":23},"end":{"line":137,"column":10}},"44":{"start":{"line":139,"column":24},"end":{"line":139,"column":57}},"45":{"start":{"line":140,"column":8},"end":{"line":140,"column":41}},"46":{"start":{"line":147,"column":24},"end":{"line":147,"column":56}},"47":{"start":{"line":149,"column":32},"end":{"line":159,"column":50}},"48":{"start":{"line":152,"column":46},"end":{"line":154,"column":null}},"49":{"start":{"line":156,"column":20},"end":{"line":156,"column":66}},"50":{"start":{"line":159,"column":29},"end":{"line":159,"column":49}},"51":{"start":{"line":161,"column":8},"end":{"line":161,"column":31}},"52":{"start":{"line":165,"column":23},"end":{"line":165,"column":54}},"53":{"start":{"line":166,"column":8},"end":{"line":167,"column":79}},"54":{"start":{"line":167,"column":12},"end":{"line":167,"column":79}},"55":{"start":{"line":169,"column":8},"end":{"line":169,"column":54}},"56":{"start":{"line":171,"column":8},"end":{"line":171,"column":22}},"57":{"start":{"line":184,"column":8},"end":{"line":184,"column":27}},"58":{"start":{"line":184,"column":20},"end":{"line":184,"column":27}},"59":{"start":{"line":187,"column":12},"end":{"line":188,"column":51}},"60":{"start":{"line":190,"column":8},"end":{"line":190,"column":32}},"61":{"start":{"line":190,"column":25},"end":{"line":190,"column":32}},"62":{"start":{"line":192,"column":34},"end":{"line":192,"column":63}},"63":{"start":{"line":193,"column":32},"end":{"line":193,"column":59}},"64":{"start":{"line":195,"column":8},"end":{"line":195,"column":57}},"65":{"start":{"line":195,"column":50},"end":{"line":195,"column":57}},"66":{"start":{"line":197,"column":26},"end":{"line":199,"column":10}},"67":{"start":{"line":201,"column":8},"end":{"line":201,"column":41}},"68":{"start":{"line":201,"column":34},"end":{"line":201,"column":41}},"69":{"start":{"line":203,"column":8},"end":{"line":205,"column":10}},"70":{"start":{"line":212,"column":8},"end":{"line":212,"column":33}},"71":{"start":{"line":212,"column":20},"end":{"line":212,"column":33}},"72":{"start":{"line":215,"column":12},"end":{"line":216,"column":51}},"73":{"start":{"line":218,"column":8},"end":{"line":218,"column":37}},"74":{"start":{"line":218,"column":25},"end":{"line":218,"column":37}},"75":{"start":{"line":220,"column":34},"end":{"line":220,"column":63}},"76":{"start":{"line":221,"column":32},"end":{"line":221,"column":59}},"77":{"start":{"line":223,"column":8},"end":{"line":223,"column":62}},"78":{"start":{"line":223,"column":50},"end":{"line":223,"column":62}},"79":{"start":{"line":225,"column":28},"end":{"line":227,"column":10}},"80":{"start":{"line":229,"column":8},"end":{"line":229,"column":48}},"81":{"start":{"line":229,"column":36},"end":{"line":229,"column":48}},"82":{"start":{"line":231,"column":26},"end":{"line":233,"column":10}},"83":{"start":{"line":235,"column":8},"end":{"line":235,"column":46}},"84":{"start":{"line":235,"column":34},"end":{"line":235,"column":46}},"85":{"start":{"line":237,"column":8},"end":{"line":237,"column":21}},"86":{"start":{"line":250,"column":8},"end":{"line":250,"column":27}},"87":{"start":{"line":250,"column":20},"end":{"line":250,"column":27}},"88":{"start":{"line":253,"column":12},"end":{"line":254,"column":51}},"89":{"start":{"line":256,"column":8},"end":{"line":256,"column":32}},"90":{"start":{"line":256,"column":25},"end":{"line":256,"column":32}},"91":{"start":{"line":258,"column":34},"end":{"line":258,"column":63}},"92":{"start":{"line":259,"column":32},"end":{"line":259,"column":59}},"93":{"start":{"line":261,"column":8},"end":{"line":261,"column":57}},"94":{"start":{"line":261,"column":50},"end":{"line":261,"column":57}},"95":{"start":{"line":263,"column":28},"end":{"line":265,"column":10}},"96":{"start":{"line":267,"column":8},"end":{"line":267,"column":43}},"97":{"start":{"line":267,"column":36},"end":{"line":267,"column":43}},"98":{"start":{"line":269,"column":26},"end":{"line":271,"column":10}},"99":{"start":{"line":273,"column":8},"end":{"line":273,"column":41}},"100":{"start":{"line":273,"column":34},"end":{"line":273,"column":41}},"101":{"start":{"line":275,"column":8},"end":{"line":277,"column":10}},"102":{"start":{"line":285,"column":23},"end":{"line":285,"column":45}},"103":{"start":{"line":287,"column":8},"end":{"line":297,"column":9}},"104":{"start":{"line":291,"column":12},"end":{"line":296,"column":14}},"105":{"start":{"line":299,"column":8},"end":{"line":312,"column":9}},"106":{"start":{"line":300,"column":26},"end":{"line":300,"column":66}},"107":{"start":{"line":301,"column":12},"end":{"line":311,"column":13}},"108":{"start":{"line":305,"column":16},"end":{"line":310,"column":18}},"109":{"start":{"line":314,"column":45},"end":{"line":350,"column":10}},"110":{"start":{"line":352,"column":30},"end":{"line":352,"column":72}},"111":{"start":{"line":355,"column":8},"end":{"line":361,"column":9}},"112":{"start":{"line":360,"column":12},"end":{"line":360,"column":42}},"113":{"start":{"line":363,"column":8},"end":{"line":375,"column":9}},"114":{"start":{"line":368,"column":12},"end":{"line":374,"column":14}},"115":{"start":{"line":377,"column":8},"end":{"line":377,"column":29}},"116":{"start":{"line":381,"column":8},"end":{"line":381,"column":31}},"117":{"start":{"line":383,"column":32},"end":{"line":383,"column":73}},"118":{"start":{"line":384,"column":8},"end":{"line":390,"column":9}},"119":{"start":{"line":385,"column":12},"end":{"line":389,"column":14}},"120":{"start":{"line":392,"column":8},"end":{"line":392,"column":42}},"121":{"start":{"line":398,"column":8},"end":{"line":411,"column":9}},"122":{"start":{"line":399,"column":26},"end":{"line":399,"column":68}},"123":{"start":{"line":400,"column":12},"end":{"line":410,"column":13}},"124":{"start":{"line":404,"column":16},"end":{"line":409,"column":18}},"125":{"start":{"line":413,"column":29},"end":{"line":415,"column":18}},"126":{"start":{"line":417,"column":22},"end":{"line":417,"column":58}},"127":{"start":{"line":418,"column":27},"end":{"line":418,"column":73}},"128":{"start":{"line":420,"column":8},"end":{"line":424,"column":9}},"129":{"start":{"line":421,"column":12},"end":{"line":423,"column":14}},"130":{"start":{"line":426,"column":25},"end":{"line":437,"column":10}},"131":{"start":{"line":439,"column":8},"end":{"line":439,"column":47}},"132":{"start":{"line":443,"column":8},"end":{"line":443,"column":44}},"133":{"start":{"line":447,"column":25},"end":{"line":447,"column":58}},"134":{"start":{"line":449,"column":8},"end":{"line":453,"column":9}},"135":{"start":{"line":450,"column":12},"end":{"line":452,"column":14}},"136":{"start":{"line":455,"column":8},"end":{"line":468,"column":9}},"137":{"start":{"line":456,"column":26},"end":{"line":456,"column":69}},"138":{"start":{"line":457,"column":12},"end":{"line":467,"column":13}},"139":{"start":{"line":461,"column":16},"end":{"line":466,"column":18}},"140":{"start":{"line":470,"column":8},"end":{"line":470,"column":44}},"141":{"start":{"line":476,"column":23},"end":{"line":476,"column":59}},"142":{"start":{"line":478,"column":8},"end":{"line":485,"column":9}},"143":{"start":{"line":479,"column":12},"end":{"line":484,"column":14}},"144":{"start":{"line":487,"column":33},"end":{"line":488,"column":null}},"145":{"start":{"line":490,"column":27},"end":{"line":491,"column":null}},"146":{"start":{"line":497,"column":34},"end":{"line":500,"column":10}},"147":{"start":{"line":502,"column":12},"end":{"line":512,"column":14}},"148":{"start":{"line":514,"column":25},"end":{"line":524,"column":10}},"149":{"start":{"line":526,"column":8},"end":{"line":526,"column":53}},"150":{"start":{"line":534,"column":8},"end":{"line":534,"column":44}},"151":{"start":{"line":535,"column":8},"end":{"line":535,"column":68}},"152":{"start":{"line":539,"column":23},"end":{"line":539,"column":51}},"153":{"start":{"line":541,"column":8},"end":{"line":548,"column":9}},"154":{"start":{"line":542,"column":12},"end":{"line":547,"column":14}},"155":{"start":{"line":550,"column":8},"end":{"line":550,"column":66}},"156":{"start":{"line":557,"column":23},"end":{"line":557,"column":66}},"157":{"start":{"line":559,"column":8},"end":{"line":569,"column":9}},"158":{"start":{"line":563,"column":12},"end":{"line":568,"column":14}},"159":{"start":{"line":571,"column":23},"end":{"line":578,"column":10}},"160":{"start":{"line":580,"column":8},"end":{"line":580,"column":49}},"161":{"start":{"line":588,"column":23},"end":{"line":588,"column":51}},"162":{"start":{"line":589,"column":8},"end":{"line":589,"column":61}},"163":{"start":{"line":591,"column":8},"end":{"line":591,"column":77}},"164":{"start":{"line":598,"column":23},"end":{"line":598,"column":51}},"165":{"start":{"line":599,"column":8},"end":{"line":599,"column":61}},"166":{"start":{"line":602,"column":12},"end":{"line":602,"column":78}},"167":{"start":{"line":604,"column":8},"end":{"line":604,"column":61}},"168":{"start":{"line":604,"column":41},"end":{"line":604,"column":56}},"169":{"start":{"line":610,"column":23},"end":{"line":610,"column":59}},"170":{"start":{"line":612,"column":8},"end":{"line":626,"column":9}},"171":{"start":{"line":618,"column":12},"end":{"line":625,"column":14}},"172":{"start":{"line":628,"column":25},"end":{"line":643,"column":10}},"173":{"start":{"line":645,"column":8},"end":{"line":645,"column":49}},"174":{"start":{"line":653,"column":8},"end":{"line":653,"column":72}},"175":{"start":{"line":655,"column":25},"end":{"line":655,"column":67}},"176":{"start":{"line":656,"column":23},"end":{"line":656,"column":60}},"177":{"start":{"line":658,"column":8},"end":{"line":668,"column":9}},"178":{"start":{"line":662,"column":12},"end":{"line":667,"column":14}},"179":{"start":{"line":670,"column":31},"end":{"line":670,"column":51}},"180":{"start":{"line":671,"column":8},"end":{"line":671,"column":41}},"181":{"start":{"line":673,"column":24},"end":{"line":673,"column":76}},"182":{"start":{"line":675,"column":8},"end":{"line":684,"column":9}},"183":{"start":{"line":676,"column":12},"end":{"line":683,"column":14}},"184":{"start":{"line":686,"column":8},"end":{"line":686,"column":23}},"185":{"start":{"line":705,"column":23},"end":{"line":705,"column":51}},"186":{"start":{"line":706,"column":27},"end":{"line":706,"column":69}},"187":{"start":{"line":707,"column":8},"end":{"line":711,"column":9}},"188":{"start":{"line":708,"column":12},"end":{"line":710,"column":14}},"189":{"start":{"line":712,"column":30},"end":{"line":712,"column":55}},"190":{"start":{"line":715,"column":35},"end":{"line":715,"column":77}},"191":{"start":{"line":716,"column":8},"end":{"line":720,"column":9}},"192":{"start":{"line":717,"column":12},"end":{"line":719,"column":14}},"193":{"start":{"line":722,"column":8},"end":{"line":724,"column":11}},"194":{"start":{"line":726,"column":8},"end":{"line":737,"column":10}},"195":{"start":{"line":750,"column":8},"end":{"line":750,"column":27}},"196":{"start":{"line":750,"column":20},"end":{"line":750,"column":27}},"197":{"start":{"line":752,"column":24},"end":{"line":752,"column":66}},"198":{"start":{"line":754,"column":8},"end":{"line":754,"column":28}},"199":{"start":{"line":754,"column":21},"end":{"line":754,"column":28}},"200":{"start":{"line":756,"column":27},"end":{"line":756,"column":69}},"201":{"start":{"line":758,"column":8},"end":{"line":758,"column":57}},"202":{"start":{"line":758,"column":50},"end":{"line":758,"column":57}},"203":{"start":{"line":760,"column":8},"end":{"line":764,"column":10}},"204":{"start":{"line":772,"column":23},"end":{"line":772,"column":51}},"205":{"start":{"line":774,"column":8},"end":{"line":778,"column":9}},"206":{"start":{"line":775,"column":12},"end":{"line":777,"column":14}},"207":{"start":{"line":780,"column":8},"end":{"line":780,"column":62}},"208":{"start":{"line":784,"column":8},"end":{"line":784,"column":46}},"209":{"start":{"line":788,"column":8},"end":{"line":788,"column":45}},"210":{"start":{"line":789,"column":25},"end":{"line":797,"column":10}},"211":{"start":{"line":798,"column":8},"end":{"line":798,"column":47}},"212":{"start":{"line":806,"column":8},"end":{"line":806,"column":59}},"213":{"start":{"line":807,"column":8},"end":{"line":807,"column":60}},"214":{"start":{"line":820,"column":8},"end":{"line":820,"column":27}},"215":{"start":{"line":820,"column":20},"end":{"line":820,"column":27}},"216":{"start":{"line":823,"column":12},"end":{"line":824,"column":51}},"217":{"start":{"line":826,"column":8},"end":{"line":826,"column":32}},"218":{"start":{"line":826,"column":25},"end":{"line":826,"column":32}},"219":{"start":{"line":828,"column":23},"end":{"line":828,"column":51}},"220":{"start":{"line":830,"column":34},"end":{"line":830,"column":63}},"221":{"start":{"line":831,"column":32},"end":{"line":831,"column":59}},"222":{"start":{"line":833,"column":8},"end":{"line":833,"column":57}},"223":{"start":{"line":833,"column":50},"end":{"line":833,"column":57}},"224":{"start":{"line":835,"column":28},"end":{"line":837,"column":10}},"225":{"start":{"line":839,"column":8},"end":{"line":839,"column":43}},"226":{"start":{"line":839,"column":36},"end":{"line":839,"column":43}},"227":{"start":{"line":841,"column":26},"end":{"line":843,"column":10}},"228":{"start":{"line":845,"column":8},"end":{"line":845,"column":41}},"229":{"start":{"line":845,"column":34},"end":{"line":845,"column":41}},"230":{"start":{"line":847,"column":8},"end":{"line":851,"column":10}},"231":{"start":{"line":855,"column":8},"end":{"line":855,"column":44}},"232":{"start":{"line":861,"column":8},"end":{"line":863,"column":9}},"233":{"start":{"line":862,"column":12},"end":{"line":862,"column":55}},"234":{"start":{"line":865,"column":22},"end":{"line":872,"column":10}},"235":{"start":{"line":874,"column":8},"end":{"line":874,"column":48}},"236":{"start":{"line":880,"column":8},"end":{"line":880,"column":46}},"237":{"start":{"line":886,"column":8},"end":{"line":886,"column":46}},"238":{"start":{"line":892,"column":8},"end":{"line":892,"column":56}},"239":{"start":{"line":896,"column":8},"end":{"line":896,"column":48}},"240":{"start":{"line":914,"column":23},"end":{"line":914,"column":51}},"241":{"start":{"line":915,"column":29},"end":{"line":915,"column":41}},"242":{"start":{"line":918,"column":35},"end":{"line":918,"column":73}},"243":{"start":{"line":920,"column":8},"end":{"line":924,"column":9}},"244":{"start":{"line":921,"column":12},"end":{"line":923,"column":14}},"245":{"start":{"line":927,"column":8},"end":{"line":945,"column":11}},"246":{"start":{"line":929,"column":12},"end":{"line":932,"column":15}},"247":{"start":{"line":935,"column":28},"end":{"line":942,"column":14}},"248":{"start":{"line":944,"column":12},"end":{"line":944,"column":52}},"249":{"start":{"line":948,"column":8},"end":{"line":958,"column":10}},"250":{"start":{"line":967,"column":8},"end":{"line":967,"column":58}},"251":{"start":{"line":976,"column":8},"end":{"line":976,"column":37}},"252":{"start":{"line":979,"column":28},"end":{"line":979,"column":77}},"253":{"start":{"line":982,"column":38},"end":{"line":985,"column":null}},"254":{"start":{"line":984,"column":16},"end":{"line":985,"column":63}},"255":{"start":{"line":987,"column":36},"end":{"line":987,"column":68}},"256":{"start":{"line":990,"column":8},"end":{"line":1005,"column":9}},"257":{"start":{"line":991,"column":12},"end":{"line":997,"column":14}},"258":{"start":{"line":999,"column":12},"end":{"line":1004,"column":14}},"259":{"start":{"line":1017,"column":8},"end":{"line":1023,"column":10}},"260":{"start":{"line":90,"column":13},"end":{"line":90,"column":26}},"261":{"start":{"line":90,"column":13},"end":{"line":1025,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":91,"column":4},"end":{"line":91,"column":null}},"loc":{"start":{"line":112,"column":52},"end":{"line":113,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":115,"column":4},"end":{"line":115,"column":9}},"loc":{"start":{"line":115,"column":45},"end":{"line":141,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":143,"column":4},"end":{"line":143,"column":9}},"loc":{"start":{"line":145,"column":26},"end":{"line":162,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":151,"column":28},"end":{"line":151,"column":33}},"loc":{"start":{"line":151,"column":45},"end":{"line":157,"column":17}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":159,"column":17},"end":{"line":159,"column":18}},"loc":{"start":{"line":159,"column":29},"end":{"line":159,"column":49}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":164,"column":4},"end":{"line":164,"column":9}},"loc":{"start":{"line":164,"column":48},"end":{"line":172,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":180,"column":12},"end":{"line":180,"column":17}},"loc":{"start":{"line":182,"column":26},"end":{"line":206,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":208,"column":12},"end":{"line":208,"column":17}},"loc":{"start":{"line":210,"column":26},"end":{"line":238,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":246,"column":12},"end":{"line":246,"column":17}},"loc":{"start":{"line":248,"column":26},"end":{"line":278,"column":5}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":280,"column":4},"end":{"line":280,"column":9}},"loc":{"start":{"line":283,"column":25},"end":{"line":378,"column":5}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":380,"column":4},"end":{"line":380,"column":9}},"loc":{"start":{"line":380,"column":27},"end":{"line":393,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":395,"column":4},"end":{"line":395,"column":9}},"loc":{"start":{"line":396,"column":38},"end":{"line":440,"column":5}}},"12":{"name":"(anonymous_16)","decl":{"start":{"line":442,"column":4},"end":{"line":442,"column":9}},"loc":{"start":{"line":442,"column":50},"end":{"line":444,"column":5}}},"13":{"name":"(anonymous_17)","decl":{"start":{"line":446,"column":4},"end":{"line":446,"column":9}},"loc":{"start":{"line":446,"column":35},"end":{"line":471,"column":5}}},"14":{"name":"(anonymous_18)","decl":{"start":{"line":473,"column":4},"end":{"line":473,"column":9}},"loc":{"start":{"line":474,"column":43},"end":{"line":527,"column":5}}},"15":{"name":"(anonymous_19)","decl":{"start":{"line":529,"column":4},"end":{"line":529,"column":9}},"loc":{"start":{"line":532,"column":26},"end":{"line":536,"column":5}}},"16":{"name":"(anonymous_20)","decl":{"start":{"line":538,"column":4},"end":{"line":538,"column":9}},"loc":{"start":{"line":538,"column":61},"end":{"line":551,"column":5}}},"17":{"name":"(anonymous_21)","decl":{"start":{"line":553,"column":4},"end":{"line":553,"column":9}},"loc":{"start":{"line":555,"column":25},"end":{"line":581,"column":5}}},"18":{"name":"(anonymous_22)","decl":{"start":{"line":583,"column":4},"end":{"line":583,"column":9}},"loc":{"start":{"line":586,"column":26},"end":{"line":592,"column":5}}},"19":{"name":"(anonymous_23)","decl":{"start":{"line":594,"column":4},"end":{"line":594,"column":9}},"loc":{"start":{"line":596,"column":26},"end":{"line":605,"column":5}}},"20":{"name":"(anonymous_24)","decl":{"start":{"line":604,"column":29},"end":{"line":604,"column":30}},"loc":{"start":{"line":604,"column":41},"end":{"line":604,"column":56}}},"21":{"name":"(anonymous_25)","decl":{"start":{"line":607,"column":4},"end":{"line":607,"column":9}},"loc":{"start":{"line":608,"column":37},"end":{"line":646,"column":5}}},"22":{"name":"(anonymous_26)","decl":{"start":{"line":648,"column":4},"end":{"line":648,"column":9}},"loc":{"start":{"line":651,"column":25},"end":{"line":687,"column":5}}},"23":{"name":"(anonymous_27)","decl":{"start":{"line":697,"column":4},"end":{"line":697,"column":9}},"loc":{"start":{"line":703,"column":69},"end":{"line":738,"column":5}}},"24":{"name":"(anonymous_28)","decl":{"start":{"line":746,"column":12},"end":{"line":746,"column":17}},"loc":{"start":{"line":748,"column":26},"end":{"line":765,"column":5}}},"25":{"name":"(anonymous_29)","decl":{"start":{"line":767,"column":4},"end":{"line":767,"column":9}},"loc":{"start":{"line":770,"column":26},"end":{"line":781,"column":5}}},"26":{"name":"(anonymous_30)","decl":{"start":{"line":783,"column":4},"end":{"line":783,"column":9}},"loc":{"start":{"line":783,"column":37},"end":{"line":785,"column":5}}},"27":{"name":"(anonymous_31)","decl":{"start":{"line":787,"column":4},"end":{"line":787,"column":9}},"loc":{"start":{"line":787,"column":49},"end":{"line":799,"column":5}}},"28":{"name":"(anonymous_32)","decl":{"start":{"line":801,"column":4},"end":{"line":801,"column":9}},"loc":{"start":{"line":804,"column":25},"end":{"line":808,"column":5}}},"29":{"name":"(anonymous_33)","decl":{"start":{"line":816,"column":12},"end":{"line":816,"column":17}},"loc":{"start":{"line":818,"column":26},"end":{"line":852,"column":5}}},"30":{"name":"(anonymous_34)","decl":{"start":{"line":854,"column":4},"end":{"line":854,"column":9}},"loc":{"start":{"line":854,"column":35},"end":{"line":856,"column":5}}},"31":{"name":"(anonymous_35)","decl":{"start":{"line":858,"column":4},"end":{"line":858,"column":9}},"loc":{"start":{"line":859,"column":42},"end":{"line":875,"column":5}}},"32":{"name":"(anonymous_36)","decl":{"start":{"line":877,"column":4},"end":{"line":877,"column":9}},"loc":{"start":{"line":878,"column":38},"end":{"line":881,"column":5}}},"33":{"name":"(anonymous_37)","decl":{"start":{"line":883,"column":4},"end":{"line":883,"column":9}},"loc":{"start":{"line":884,"column":18},"end":{"line":887,"column":5}}},"34":{"name":"(anonymous_38)","decl":{"start":{"line":889,"column":4},"end":{"line":889,"column":9}},"loc":{"start":{"line":890,"column":23},"end":{"line":893,"column":5}}},"35":{"name":"(anonymous_39)","decl":{"start":{"line":895,"column":4},"end":{"line":895,"column":9}},"loc":{"start":{"line":895,"column":39},"end":{"line":897,"column":5}}},"36":{"name":"(anonymous_40)","decl":{"start":{"line":907,"column":4},"end":{"line":907,"column":9}},"loc":{"start":{"line":912,"column":23},"end":{"line":959,"column":5}}},"37":{"name":"(anonymous_41)","decl":{"start":{"line":927,"column":39},"end":{"line":927,"column":44}},"loc":{"start":{"line":927,"column":52},"end":{"line":945,"column":9}}},"38":{"name":"(anonymous_42)","decl":{"start":{"line":964,"column":4},"end":{"line":964,"column":9}},"loc":{"start":{"line":965,"column":24},"end":{"line":968,"column":5}}},"39":{"name":"(anonymous_43)","decl":{"start":{"line":975,"column":4},"end":{"line":975,"column":9}},"loc":{"start":{"line":975,"column":41},"end":{"line":1006,"column":5}}},"40":{"name":"(anonymous_44)","decl":{"start":{"line":983,"column":12},"end":{"line":983,"column":13}},"loc":{"start":{"line":984,"column":16},"end":{"line":985,"column":63}}},"41":{"name":"(anonymous_45)","decl":{"start":{"line":1012,"column":4},"end":{"line":1012,"column":9}},"loc":{"start":{"line":1015,"column":25},"end":{"line":1024,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":116,"column":8},"end":{"line":118,"column":9}},"type":"if","locations":[{"start":{"line":116,"column":8},"end":{"line":118,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":128,"column":20},"end":{"line":128,"column":42}},"type":"binary-expr","locations":[{"start":{"line":128,"column":20},"end":{"line":128,"column":34}},{"start":{"line":128,"column":38},"end":{"line":128,"column":42}}]},"2":{"loc":{"start":{"line":129,"column":23},"end":{"line":129,"column":48}},"type":"binary-expr","locations":[{"start":{"line":129,"column":23},"end":{"line":129,"column":40}},{"start":{"line":129,"column":44},"end":{"line":129,"column":48}}]},"3":{"loc":{"start":{"line":130,"column":23},"end":{"line":130,"column":48}},"type":"binary-expr","locations":[{"start":{"line":130,"column":23},"end":{"line":130,"column":40}},{"start":{"line":130,"column":44},"end":{"line":130,"column":48}}]},"4":{"loc":{"start":{"line":131,"column":29},"end":{"line":131,"column":60}},"type":"binary-expr","locations":[{"start":{"line":131,"column":29},"end":{"line":131,"column":52}},{"start":{"line":131,"column":56},"end":{"line":131,"column":60}}]},"5":{"loc":{"start":{"line":132,"column":31},"end":{"line":132,"column":64}},"type":"binary-expr","locations":[{"start":{"line":132,"column":31},"end":{"line":132,"column":56}},{"start":{"line":132,"column":60},"end":{"line":132,"column":64}}]},"6":{"loc":{"start":{"line":133,"column":34},"end":{"line":133,"column":70}},"type":"binary-expr","locations":[{"start":{"line":133,"column":34},"end":{"line":133,"column":62}},{"start":{"line":133,"column":66},"end":{"line":133,"column":70}}]},"7":{"loc":{"start":{"line":134,"column":21},"end":{"line":134,"column":44}},"type":"binary-expr","locations":[{"start":{"line":134,"column":21},"end":{"line":134,"column":36}},{"start":{"line":134,"column":40},"end":{"line":134,"column":44}}]},"8":{"loc":{"start":{"line":135,"column":19},"end":{"line":135,"column":51}},"type":"binary-expr","locations":[{"start":{"line":135,"column":19},"end":{"line":135,"column":32}},{"start":{"line":135,"column":36},"end":{"line":135,"column":51}}]},"9":{"loc":{"start":{"line":136,"column":22},"end":{"line":136,"column":46}},"type":"binary-expr","locations":[{"start":{"line":136,"column":22},"end":{"line":136,"column":38}},{"start":{"line":136,"column":42},"end":{"line":136,"column":46}}]},"10":{"loc":{"start":{"line":156,"column":27},"end":{"line":156,"column":65}},"type":"cond-expr","locations":[{"start":{"line":156,"column":47},"end":{"line":156,"column":53}},{"start":{"line":156,"column":56},"end":{"line":156,"column":65}}]},"11":{"loc":{"start":{"line":166,"column":8},"end":{"line":167,"column":79}},"type":"if","locations":[{"start":{"line":166,"column":8},"end":{"line":167,"column":79}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":184,"column":8},"end":{"line":184,"column":27}},"type":"if","locations":[{"start":{"line":184,"column":8},"end":{"line":184,"column":27}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":187,"column":12},"end":{"line":188,"column":51}},"type":"binary-expr","locations":[{"start":{"line":187,"column":12},"end":{"line":187,"column":54}},{"start":{"line":188,"column":12},"end":{"line":188,"column":51}}]},"14":{"loc":{"start":{"line":190,"column":8},"end":{"line":190,"column":32}},"type":"if","locations":[{"start":{"line":190,"column":8},"end":{"line":190,"column":32}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":195,"column":8},"end":{"line":195,"column":57}},"type":"if","locations":[{"start":{"line":195,"column":8},"end":{"line":195,"column":57}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":195,"column":12},"end":{"line":195,"column":48}},"type":"binary-expr","locations":[{"start":{"line":195,"column":12},"end":{"line":195,"column":29}},{"start":{"line":195,"column":33},"end":{"line":195,"column":48}}]},"17":{"loc":{"start":{"line":201,"column":8},"end":{"line":201,"column":41}},"type":"if","locations":[{"start":{"line":201,"column":8},"end":{"line":201,"column":41}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":212,"column":8},"end":{"line":212,"column":33}},"type":"if","locations":[{"start":{"line":212,"column":8},"end":{"line":212,"column":33}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":215,"column":12},"end":{"line":216,"column":51}},"type":"binary-expr","locations":[{"start":{"line":215,"column":12},"end":{"line":215,"column":54}},{"start":{"line":216,"column":12},"end":{"line":216,"column":51}}]},"20":{"loc":{"start":{"line":218,"column":8},"end":{"line":218,"column":37}},"type":"if","locations":[{"start":{"line":218,"column":8},"end":{"line":218,"column":37}},{"start":{},"end":{}}]},"21":{"loc":{"start":{"line":223,"column":8},"end":{"line":223,"column":62}},"type":"if","locations":[{"start":{"line":223,"column":8},"end":{"line":223,"column":62}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":223,"column":12},"end":{"line":223,"column":48}},"type":"binary-expr","locations":[{"start":{"line":223,"column":12},"end":{"line":223,"column":29}},{"start":{"line":223,"column":33},"end":{"line":223,"column":48}}]},"23":{"loc":{"start":{"line":229,"column":8},"end":{"line":229,"column":48}},"type":"if","locations":[{"start":{"line":229,"column":8},"end":{"line":229,"column":48}},{"start":{},"end":{}}]},"24":{"loc":{"start":{"line":235,"column":8},"end":{"line":235,"column":46}},"type":"if","locations":[{"start":{"line":235,"column":8},"end":{"line":235,"column":46}},{"start":{},"end":{}}]},"25":{"loc":{"start":{"line":250,"column":8},"end":{"line":250,"column":27}},"type":"if","locations":[{"start":{"line":250,"column":8},"end":{"line":250,"column":27}},{"start":{},"end":{}}]},"26":{"loc":{"start":{"line":253,"column":12},"end":{"line":254,"column":51}},"type":"binary-expr","locations":[{"start":{"line":253,"column":12},"end":{"line":253,"column":54}},{"start":{"line":254,"column":12},"end":{"line":254,"column":51}}]},"27":{"loc":{"start":{"line":256,"column":8},"end":{"line":256,"column":32}},"type":"if","locations":[{"start":{"line":256,"column":8},"end":{"line":256,"column":32}},{"start":{},"end":{}}]},"28":{"loc":{"start":{"line":261,"column":8},"end":{"line":261,"column":57}},"type":"if","locations":[{"start":{"line":261,"column":8},"end":{"line":261,"column":57}},{"start":{},"end":{}}]},"29":{"loc":{"start":{"line":261,"column":12},"end":{"line":261,"column":48}},"type":"binary-expr","locations":[{"start":{"line":261,"column":12},"end":{"line":261,"column":29}},{"start":{"line":261,"column":33},"end":{"line":261,"column":48}}]},"30":{"loc":{"start":{"line":267,"column":8},"end":{"line":267,"column":43}},"type":"if","locations":[{"start":{"line":267,"column":8},"end":{"line":267,"column":43}},{"start":{},"end":{}}]},"31":{"loc":{"start":{"line":273,"column":8},"end":{"line":273,"column":41}},"type":"if","locations":[{"start":{"line":273,"column":8},"end":{"line":273,"column":41}},{"start":{},"end":{}}]},"32":{"loc":{"start":{"line":287,"column":8},"end":{"line":297,"column":9}},"type":"if","locations":[{"start":{"line":287,"column":8},"end":{"line":297,"column":9}},{"start":{},"end":{}}]},"33":{"loc":{"start":{"line":288,"column":12},"end":{"line":289,"column":57}},"type":"binary-expr","locations":[{"start":{"line":288,"column":12},"end":{"line":288,"column":44}},{"start":{"line":289,"column":12},"end":{"line":289,"column":57}}]},"34":{"loc":{"start":{"line":299,"column":8},"end":{"line":312,"column":9}},"type":"if","locations":[{"start":{"line":299,"column":8},"end":{"line":312,"column":9}},{"start":{},"end":{}}]},"35":{"loc":{"start":{"line":301,"column":12},"end":{"line":311,"column":13}},"type":"if","locations":[{"start":{"line":301,"column":12},"end":{"line":311,"column":13}},{"start":{},"end":{}}]},"36":{"loc":{"start":{"line":302,"column":16},"end":{"line":303,"column":49}},"type":"binary-expr","locations":[{"start":{"line":302,"column":16},"end":{"line":302,"column":46}},{"start":{"line":303,"column":16},"end":{"line":303,"column":49}}]},"37":{"loc":{"start":{"line":315,"column":16},"end":{"line":315,"column":77}},"type":"cond-expr","locations":[{"start":{"line":315,"column":46},"end":{"line":315,"column":72}},{"start":{"line":315,"column":75},"end":{"line":315,"column":77}}]},"38":{"loc":{"start":{"line":316,"column":16},"end":{"line":318,"column":20}},"type":"cond-expr","locations":[{"start":{"line":317,"column":18},"end":{"line":317,"column":56}},{"start":{"line":318,"column":18},"end":{"line":318,"column":20}}]},"39":{"loc":{"start":{"line":319,"column":16},"end":{"line":321,"column":20}},"type":"cond-expr","locations":[{"start":{"line":320,"column":18},"end":{"line":320,"column":60}},{"start":{"line":321,"column":18},"end":{"line":321,"column":20}}]},"40":{"loc":{"start":{"line":322,"column":16},"end":{"line":324,"column":20}},"type":"cond-expr","locations":[{"start":{"line":323,"column":18},"end":{"line":323,"column":66}},{"start":{"line":324,"column":18},"end":{"line":324,"column":20}}]},"41":{"loc":{"start":{"line":325,"column":16},"end":{"line":325,"column":68}},"type":"cond-expr","locations":[{"start":{"line":325,"column":43},"end":{"line":325,"column":63}},{"start":{"line":325,"column":66},"end":{"line":325,"column":68}}]},"42":{"loc":{"start":{"line":326,"column":16},"end":{"line":328,"column":20}},"type":"cond-expr","locations":[{"start":{"line":327,"column":18},"end":{"line":327,"column":50}},{"start":{"line":328,"column":18},"end":{"line":328,"column":20}}]},"43":{"loc":{"start":{"line":329,"column":16},"end":{"line":329,"column":74}},"type":"cond-expr","locations":[{"start":{"line":329,"column":45},"end":{"line":329,"column":69}},{"start":{"line":329,"column":72},"end":{"line":329,"column":74}}]},"44":{"loc":{"start":{"line":330,"column":16},"end":{"line":330,"column":74}},"type":"cond-expr","locations":[{"start":{"line":330,"column":45},"end":{"line":330,"column":69}},{"start":{"line":330,"column":72},"end":{"line":330,"column":74}}]},"45":{"loc":{"start":{"line":331,"column":16},"end":{"line":333,"column":20}},"type":"cond-expr","locations":[{"start":{"line":332,"column":18},"end":{"line":332,"column":48}},{"start":{"line":333,"column":18},"end":{"line":333,"column":20}}]},"46":{"loc":{"start":{"line":334,"column":16},"end":{"line":336,"column":20}},"type":"cond-expr","locations":[{"start":{"line":335,"column":18},"end":{"line":335,"column":48}},{"start":{"line":336,"column":18},"end":{"line":336,"column":20}}]},"47":{"loc":{"start":{"line":337,"column":16},"end":{"line":339,"column":20}},"type":"cond-expr","locations":[{"start":{"line":338,"column":18},"end":{"line":338,"column":60}},{"start":{"line":339,"column":18},"end":{"line":339,"column":20}}]},"48":{"loc":{"start":{"line":340,"column":16},"end":{"line":342,"column":20}},"type":"cond-expr","locations":[{"start":{"line":341,"column":18},"end":{"line":341,"column":64}},{"start":{"line":342,"column":18},"end":{"line":342,"column":20}}]},"49":{"loc":{"start":{"line":343,"column":16},"end":{"line":345,"column":20}},"type":"cond-expr","locations":[{"start":{"line":344,"column":18},"end":{"line":344,"column":70}},{"start":{"line":345,"column":18},"end":{"line":345,"column":20}}]},"50":{"loc":{"start":{"line":346,"column":16},"end":{"line":346,"column":77}},"type":"cond-expr","locations":[{"start":{"line":346,"column":46},"end":{"line":346,"column":72}},{"start":{"line":346,"column":75},"end":{"line":346,"column":77}}]},"51":{"loc":{"start":{"line":347,"column":16},"end":{"line":349,"column":20}},"type":"cond-expr","locations":[{"start":{"line":348,"column":18},"end":{"line":348,"column":46}},{"start":{"line":349,"column":18},"end":{"line":349,"column":20}}]},"52":{"loc":{"start":{"line":355,"column":8},"end":{"line":361,"column":9}},"type":"if","locations":[{"start":{"line":355,"column":8},"end":{"line":361,"column":9}},{"start":{},"end":{}}]},"53":{"loc":{"start":{"line":356,"column":12},"end":{"line":358,"column":48}},"type":"binary-expr","locations":[{"start":{"line":356,"column":12},"end":{"line":356,"column":23}},{"start":{"line":357,"column":12},"end":{"line":357,"column":47}},{"start":{"line":358,"column":12},"end":{"line":358,"column":48}}]},"54":{"loc":{"start":{"line":363,"column":8},"end":{"line":375,"column":9}},"type":"if","locations":[{"start":{"line":363,"column":8},"end":{"line":375,"column":9}},{"start":{},"end":{}}]},"55":{"loc":{"start":{"line":364,"column":12},"end":{"line":366,"column":48}},"type":"binary-expr","locations":[{"start":{"line":364,"column":12},"end":{"line":364,"column":23}},{"start":{"line":365,"column":12},"end":{"line":365,"column":47}},{"start":{"line":366,"column":12},"end":{"line":366,"column":48}}]},"56":{"loc":{"start":{"line":384,"column":8},"end":{"line":390,"column":9}},"type":"if","locations":[{"start":{"line":384,"column":8},"end":{"line":390,"column":9}},{"start":{},"end":{}}]},"57":{"loc":{"start":{"line":398,"column":8},"end":{"line":411,"column":9}},"type":"if","locations":[{"start":{"line":398,"column":8},"end":{"line":411,"column":9}},{"start":{},"end":{}}]},"58":{"loc":{"start":{"line":400,"column":12},"end":{"line":410,"column":13}},"type":"if","locations":[{"start":{"line":400,"column":12},"end":{"line":410,"column":13}},{"start":{},"end":{}}]},"59":{"loc":{"start":{"line":401,"column":16},"end":{"line":402,"column":49}},"type":"binary-expr","locations":[{"start":{"line":401,"column":16},"end":{"line":401,"column":46}},{"start":{"line":402,"column":16},"end":{"line":402,"column":49}}]},"60":{"loc":{"start":{"line":413,"column":29},"end":{"line":415,"column":18}},"type":"cond-expr","locations":[{"start":{"line":414,"column":14},"end":{"line":414,"column":78}},{"start":{"line":415,"column":14},"end":{"line":415,"column":18}}]},"61":{"loc":{"start":{"line":417,"column":22},"end":{"line":417,"column":58}},"type":"binary-expr","locations":[{"start":{"line":417,"column":22},"end":{"line":417,"column":35}},{"start":{"line":417,"column":39},"end":{"line":417,"column":58}}]},"62":{"loc":{"start":{"line":418,"column":27},"end":{"line":418,"column":73}},"type":"binary-expr","locations":[{"start":{"line":418,"column":27},"end":{"line":418,"column":45}},{"start":{"line":418,"column":49},"end":{"line":418,"column":73}}]},"63":{"loc":{"start":{"line":420,"column":8},"end":{"line":424,"column":9}},"type":"if","locations":[{"start":{"line":420,"column":8},"end":{"line":424,"column":9}},{"start":{},"end":{}}]},"64":{"loc":{"start":{"line":420,"column":12},"end":{"line":420,"column":33}},"type":"binary-expr","locations":[{"start":{"line":420,"column":12},"end":{"line":420,"column":18}},{"start":{"line":420,"column":22},"end":{"line":420,"column":33}}]},"65":{"loc":{"start":{"line":427,"column":21},"end":{"line":427,"column":44}},"type":"binary-expr","locations":[{"start":{"line":427,"column":21},"end":{"line":427,"column":36}},{"start":{"line":427,"column":40},"end":{"line":427,"column":44}}]},"66":{"loc":{"start":{"line":428,"column":32},"end":{"line":428,"column":66}},"type":"binary-expr","locations":[{"start":{"line":428,"column":32},"end":{"line":428,"column":58}},{"start":{"line":428,"column":62},"end":{"line":428,"column":66}}]},"67":{"loc":{"start":{"line":432,"column":16},"end":{"line":432,"column":74}},"type":"binary-expr","locations":[{"start":{"line":432,"column":16},"end":{"line":432,"column":36}},{"start":{"line":432,"column":40},"end":{"line":432,"column":66}},{"start":{"line":432,"column":70},"end":{"line":432,"column":74}}]},"68":{"loc":{"start":{"line":434,"column":16},"end":{"line":436,"column":21}},"type":"binary-expr","locations":[{"start":{"line":434,"column":16},"end":{"line":434,"column":43}},{"start":{"line":435,"column":16},"end":{"line":435,"column":49}},{"start":{"line":436,"column":16},"end":{"line":436,"column":21}}]},"69":{"loc":{"start":{"line":449,"column":8},"end":{"line":453,"column":9}},"type":"if","locations":[{"start":{"line":449,"column":8},"end":{"line":453,"column":9}},{"start":{},"end":{}}]},"70":{"loc":{"start":{"line":455,"column":8},"end":{"line":468,"column":9}},"type":"if","locations":[{"start":{"line":455,"column":8},"end":{"line":468,"column":9}},{"start":{},"end":{}}]},"71":{"loc":{"start":{"line":457,"column":12},"end":{"line":467,"column":13}},"type":"if","locations":[{"start":{"line":457,"column":12},"end":{"line":467,"column":13}},{"start":{},"end":{}}]},"72":{"loc":{"start":{"line":458,"column":16},"end":{"line":459,"column":49}},"type":"binary-expr","locations":[{"start":{"line":458,"column":16},"end":{"line":458,"column":46}},{"start":{"line":459,"column":16},"end":{"line":459,"column":49}}]},"73":{"loc":{"start":{"line":478,"column":8},"end":{"line":485,"column":9}},"type":"if","locations":[{"start":{"line":478,"column":8},"end":{"line":485,"column":9}},{"start":{},"end":{}}]},"74":{"loc":{"start":{"line":498,"column":21},"end":{"line":498,"column":48}},"type":"binary-expr","locations":[{"start":{"line":498,"column":21},"end":{"line":498,"column":35}},{"start":{"line":498,"column":39},"end":{"line":498,"column":48}}]},"75":{"loc":{"start":{"line":502,"column":12},"end":{"line":512,"column":14}},"type":"binary-expr","locations":[{"start":{"line":502,"column":12},"end":{"line":502,"column":32}},{"start":{"line":503,"column":13},"end":{"line":511,"column":null}}]},"76":{"loc":{"start":{"line":505,"column":29},"end":{"line":505,"column":51}},"type":"binary-expr","locations":[{"start":{"line":505,"column":29},"end":{"line":505,"column":43}},{"start":{"line":505,"column":47},"end":{"line":505,"column":51}}]},"77":{"loc":{"start":{"line":541,"column":8},"end":{"line":548,"column":9}},"type":"if","locations":[{"start":{"line":541,"column":8},"end":{"line":548,"column":9}},{"start":{},"end":{}}]},"78":{"loc":{"start":{"line":559,"column":8},"end":{"line":569,"column":9}},"type":"if","locations":[{"start":{"line":559,"column":8},"end":{"line":569,"column":9}},{"start":{},"end":{}}]},"79":{"loc":{"start":{"line":560,"column":12},"end":{"line":561,"column":56}},"type":"binary-expr","locations":[{"start":{"line":560,"column":12},"end":{"line":560,"column":52}},{"start":{"line":561,"column":12},"end":{"line":561,"column":56}}]},"80":{"loc":{"start":{"line":576,"column":28},"end":{"line":576,"column":58}},"type":"binary-expr","locations":[{"start":{"line":576,"column":28},"end":{"line":576,"column":50}},{"start":{"line":576,"column":54},"end":{"line":576,"column":58}}]},"81":{"loc":{"start":{"line":577,"column":23},"end":{"line":577,"column":48}},"type":"binary-expr","locations":[{"start":{"line":577,"column":23},"end":{"line":577,"column":40}},{"start":{"line":577,"column":44},"end":{"line":577,"column":48}}]},"82":{"loc":{"start":{"line":612,"column":8},"end":{"line":626,"column":9}},"type":"if","locations":[{"start":{"line":612,"column":8},"end":{"line":626,"column":9}},{"start":{},"end":{}}]},"83":{"loc":{"start":{"line":613,"column":12},"end":{"line":616,"column":56}},"type":"binary-expr","locations":[{"start":{"line":613,"column":12},"end":{"line":613,"column":44}},{"start":{"line":614,"column":12},"end":{"line":614,"column":52}},{"start":{"line":615,"column":12},"end":{"line":615,"column":57}},{"start":{"line":616,"column":12},"end":{"line":616,"column":56}}]},"84":{"loc":{"start":{"line":658,"column":8},"end":{"line":668,"column":9}},"type":"if","locations":[{"start":{"line":658,"column":8},"end":{"line":668,"column":9}},{"start":{},"end":{}}]},"85":{"loc":{"start":{"line":659,"column":12},"end":{"line":660,"column":56}},"type":"binary-expr","locations":[{"start":{"line":659,"column":12},"end":{"line":659,"column":52}},{"start":{"line":660,"column":12},"end":{"line":660,"column":56}}]},"86":{"loc":{"start":{"line":675,"column":8},"end":{"line":684,"column":9}},"type":"if","locations":[{"start":{"line":675,"column":8},"end":{"line":684,"column":9}},{"start":{},"end":{}}]},"87":{"loc":{"start":{"line":701,"column":8},"end":{"line":701,"column":41}},"type":"default-arg","locations":[{"start":{"line":701,"column":26},"end":{"line":701,"column":41}}]},"88":{"loc":{"start":{"line":702,"column":8},"end":{"line":702,"column":45}},"type":"default-arg","locations":[{"start":{"line":702,"column":28},"end":{"line":702,"column":45}}]},"89":{"loc":{"start":{"line":703,"column":8},"end":{"line":703,"column":69}},"type":"default-arg","locations":[{"start":{"line":703,"column":25},"end":{"line":703,"column":69}}]},"90":{"loc":{"start":{"line":707,"column":8},"end":{"line":711,"column":9}},"type":"if","locations":[{"start":{"line":707,"column":8},"end":{"line":711,"column":9}},{"start":{},"end":{}}]},"91":{"loc":{"start":{"line":716,"column":8},"end":{"line":720,"column":9}},"type":"if","locations":[{"start":{"line":716,"column":8},"end":{"line":720,"column":9}},{"start":{},"end":{}}]},"92":{"loc":{"start":{"line":750,"column":8},"end":{"line":750,"column":27}},"type":"if","locations":[{"start":{"line":750,"column":8},"end":{"line":750,"column":27}},{"start":{},"end":{}}]},"93":{"loc":{"start":{"line":754,"column":8},"end":{"line":754,"column":28}},"type":"if","locations":[{"start":{"line":754,"column":8},"end":{"line":754,"column":28}},{"start":{},"end":{}}]},"94":{"loc":{"start":{"line":758,"column":8},"end":{"line":758,"column":57}},"type":"if","locations":[{"start":{"line":758,"column":8},"end":{"line":758,"column":57}},{"start":{},"end":{}}]},"95":{"loc":{"start":{"line":774,"column":8},"end":{"line":778,"column":9}},"type":"if","locations":[{"start":{"line":774,"column":8},"end":{"line":778,"column":9}},{"start":{},"end":{}}]},"96":{"loc":{"start":{"line":820,"column":8},"end":{"line":820,"column":27}},"type":"if","locations":[{"start":{"line":820,"column":8},"end":{"line":820,"column":27}},{"start":{},"end":{}}]},"97":{"loc":{"start":{"line":823,"column":12},"end":{"line":824,"column":51}},"type":"binary-expr","locations":[{"start":{"line":823,"column":12},"end":{"line":823,"column":54}},{"start":{"line":824,"column":12},"end":{"line":824,"column":51}}]},"98":{"loc":{"start":{"line":826,"column":8},"end":{"line":826,"column":32}},"type":"if","locations":[{"start":{"line":826,"column":8},"end":{"line":826,"column":32}},{"start":{},"end":{}}]},"99":{"loc":{"start":{"line":833,"column":8},"end":{"line":833,"column":57}},"type":"if","locations":[{"start":{"line":833,"column":8},"end":{"line":833,"column":57}},{"start":{},"end":{}}]},"100":{"loc":{"start":{"line":833,"column":12},"end":{"line":833,"column":48}},"type":"binary-expr","locations":[{"start":{"line":833,"column":12},"end":{"line":833,"column":29}},{"start":{"line":833,"column":33},"end":{"line":833,"column":48}}]},"101":{"loc":{"start":{"line":839,"column":8},"end":{"line":839,"column":43}},"type":"if","locations":[{"start":{"line":839,"column":8},"end":{"line":839,"column":43}},{"start":{},"end":{}}]},"102":{"loc":{"start":{"line":845,"column":8},"end":{"line":845,"column":41}},"type":"if","locations":[{"start":{"line":845,"column":8},"end":{"line":845,"column":41}},{"start":{},"end":{}}]},"103":{"loc":{"start":{"line":861,"column":8},"end":{"line":863,"column":9}},"type":"if","locations":[{"start":{"line":861,"column":8},"end":{"line":863,"column":9}},{"start":{},"end":{}}]},"104":{"loc":{"start":{"line":866,"column":21},"end":{"line":866,"column":44}},"type":"binary-expr","locations":[{"start":{"line":866,"column":21},"end":{"line":866,"column":36}},{"start":{"line":866,"column":40},"end":{"line":866,"column":44}}]},"105":{"loc":{"start":{"line":871,"column":26},"end":{"line":871,"column":51}},"type":"binary-expr","locations":[{"start":{"line":871,"column":26},"end":{"line":871,"column":46}},{"start":{"line":871,"column":50},"end":{"line":871,"column":51}}]},"106":{"loc":{"start":{"line":910,"column":8},"end":{"line":910,"column":41}},"type":"default-arg","locations":[{"start":{"line":910,"column":26},"end":{"line":910,"column":41}}]},"107":{"loc":{"start":{"line":911,"column":8},"end":{"line":911,"column":52}},"type":"default-arg","locations":[{"start":{"line":911,"column":35},"end":{"line":911,"column":52}}]},"108":{"loc":{"start":{"line":920,"column":8},"end":{"line":924,"column":9}},"type":"if","locations":[{"start":{"line":920,"column":8},"end":{"line":924,"column":9}},{"start":{},"end":{}}]},"109":{"loc":{"start":{"line":939,"column":29},"end":{"line":939,"column":44}},"type":"binary-expr","locations":[{"start":{"line":939,"column":29},"end":{"line":939,"column":36}},{"start":{"line":939,"column":40},"end":{"line":939,"column":44}}]},"110":{"loc":{"start":{"line":984,"column":16},"end":{"line":985,"column":63}},"type":"binary-expr","locations":[{"start":{"line":984,"column":16},"end":{"line":984,"column":59}},{"start":{"line":985,"column":16},"end":{"line":985,"column":63}}]},"111":{"loc":{"start":{"line":990,"column":8},"end":{"line":1005,"column":9}},"type":"if","locations":[{"start":{"line":990,"column":8},"end":{"line":1005,"column":9}},{"start":{"line":998,"column":15},"end":{"line":1005,"column":9}}]},"112":{"loc":{"start":{"line":990,"column":12},"end":{"line":990,"column":58}},"type":"binary-expr","locations":[{"start":{"line":990,"column":12},"end":{"line":990,"column":32}},{"start":{"line":990,"column":36},"end":{"line":990,"column":58}}]},"113":{"loc":{"start":{"line":93,"column":34},"end":{"line":93,"column":null}},"type":"cond-expr","locations":[{"start":{"line":93,"column":54},"end":{"line":93,"column":null}},{"start":{"line":93,"column":54},"end":{"line":93,"column":null}}]},"114":{"loc":{"start":{"line":93,"column":34},"end":{"line":93,"column":54}},"type":"binary-expr","locations":[{"start":{"line":93,"column":34},"end":{"line":93,"column":null}},{"start":{"line":93,"column":34},"end":{"line":93,"column":54}}]},"115":{"loc":{"start":{"line":93,"column":54},"end":{"line":95,"column":null}},"type":"cond-expr","locations":[{"start":{"line":95,"column":58},"end":{"line":95,"column":null}},{"start":{"line":95,"column":58},"end":{"line":95,"column":null}}]},"116":{"loc":{"start":{"line":93,"column":54},"end":{"line":95,"column":58}},"type":"binary-expr","locations":[{"start":{"line":93,"column":54},"end":{"line":95,"column":null}},{"start":{"line":95,"column":36},"end":{"line":95,"column":58}}]},"117":{"loc":{"start":{"line":95,"column":58},"end":{"line":97,"column":null}},"type":"cond-expr","locations":[{"start":{"line":97,"column":80},"end":{"line":97,"column":null}},{"start":{"line":97,"column":80},"end":{"line":97,"column":null}}]},"118":{"loc":{"start":{"line":95,"column":58},"end":{"line":97,"column":80}},"type":"binary-expr","locations":[{"start":{"line":95,"column":58},"end":{"line":97,"column":null}},{"start":{"line":97,"column":44},"end":{"line":97,"column":80}}]},"119":{"loc":{"start":{"line":97,"column":80},"end":{"line":99,"column":null}},"type":"cond-expr","locations":[{"start":{"line":99,"column":54},"end":{"line":99,"column":null}},{"start":{"line":99,"column":54},"end":{"line":99,"column":null}}]},"120":{"loc":{"start":{"line":97,"column":80},"end":{"line":99,"column":54}},"type":"binary-expr","locations":[{"start":{"line":97,"column":80},"end":{"line":99,"column":null}},{"start":{"line":99,"column":34},"end":{"line":99,"column":54}}]},"121":{"loc":{"start":{"line":99,"column":54},"end":{"line":101,"column":null}},"type":"cond-expr","locations":[{"start":{"line":101,"column":62},"end":{"line":101,"column":null}},{"start":{"line":101,"column":62},"end":{"line":101,"column":null}}]},"122":{"loc":{"start":{"line":99,"column":54},"end":{"line":101,"column":62}},"type":"binary-expr","locations":[{"start":{"line":99,"column":54},"end":{"line":101,"column":null}},{"start":{"line":101,"column":38},"end":{"line":101,"column":62}}]},"123":{"loc":{"start":{"line":101,"column":62},"end":{"line":103,"column":null}},"type":"cond-expr","locations":[{"start":{"line":103,"column":58},"end":{"line":103,"column":null}},{"start":{"line":103,"column":58},"end":{"line":103,"column":null}}]},"124":{"loc":{"start":{"line":101,"column":62},"end":{"line":103,"column":58}},"type":"binary-expr","locations":[{"start":{"line":101,"column":62},"end":{"line":103,"column":null}},{"start":{"line":103,"column":36},"end":{"line":103,"column":58}}]},"125":{"loc":{"start":{"line":103,"column":58},"end":{"line":105,"column":null}},"type":"cond-expr","locations":[{"start":{"line":105,"column":66},"end":{"line":105,"column":null}},{"start":{"line":105,"column":66},"end":{"line":105,"column":null}}]},"126":{"loc":{"start":{"line":103,"column":58},"end":{"line":105,"column":66}},"type":"binary-expr","locations":[{"start":{"line":103,"column":58},"end":{"line":105,"column":null}},{"start":{"line":105,"column":40},"end":{"line":105,"column":66}}]},"127":{"loc":{"start":{"line":105,"column":66},"end":{"line":107,"column":null}},"type":"cond-expr","locations":[{"start":{"line":107,"column":71},"end":{"line":107,"column":null}},{"start":{"line":107,"column":71},"end":{"line":107,"column":null}}]},"128":{"loc":{"start":{"line":105,"column":66},"end":{"line":107,"column":71}},"type":"binary-expr","locations":[{"start":{"line":105,"column":66},"end":{"line":107,"column":null}},{"start":{"line":107,"column":39},"end":{"line":107,"column":71}}]},"129":{"loc":{"start":{"line":107,"column":71},"end":{"line":108,"column":null}},"type":"cond-expr","locations":[{"start":{"line":108,"column":46},"end":{"line":108,"column":null}},{"start":{"line":108,"column":46},"end":{"line":108,"column":null}}]},"130":{"loc":{"start":{"line":107,"column":71},"end":{"line":108,"column":46}},"type":"binary-expr","locations":[{"start":{"line":107,"column":71},"end":{"line":108,"column":null}},{"start":{"line":108,"column":33},"end":{"line":108,"column":46}}]},"131":{"loc":{"start":{"line":108,"column":46},"end":{"line":109,"column":null}},"type":"cond-expr","locations":[{"start":{"line":109,"column":67},"end":{"line":109,"column":null}},{"start":{"line":109,"column":67},"end":{"line":109,"column":null}}]},"132":{"loc":{"start":{"line":108,"column":46},"end":{"line":109,"column":67}},"type":"binary-expr","locations":[{"start":{"line":108,"column":46},"end":{"line":109,"column":null}},{"start":{"line":109,"column":44},"end":{"line":109,"column":67}}]},"133":{"loc":{"start":{"line":109,"column":67},"end":{"line":110,"column":null}},"type":"cond-expr","locations":[{"start":{"line":110,"column":55},"end":{"line":110,"column":null}},{"start":{"line":110,"column":55},"end":{"line":110,"column":null}}]},"134":{"loc":{"start":{"line":109,"column":67},"end":{"line":110,"column":55}},"type":"binary-expr","locations":[{"start":{"line":109,"column":67},"end":{"line":110,"column":null}},{"start":{"line":110,"column":38},"end":{"line":110,"column":55}}]},"135":{"loc":{"start":{"line":110,"column":55},"end":{"line":111,"column":null}},"type":"cond-expr","locations":[{"start":{"line":111,"column":45},"end":{"line":111,"column":null}},{"start":{"line":111,"column":45},"end":{"line":111,"column":null}}]},"136":{"loc":{"start":{"line":110,"column":55},"end":{"line":111,"column":45}},"type":"binary-expr","locations":[{"start":{"line":110,"column":55},"end":{"line":111,"column":null}},{"start":{"line":111,"column":33},"end":{"line":111,"column":45}}]},"137":{"loc":{"start":{"line":111,"column":45},"end":{"line":112,"column":null}},"type":"cond-expr","locations":[{"start":{"line":112,"column":52},"end":{"line":112,"column":null}},{"start":{"line":112,"column":52},"end":{"line":112,"column":null}}]},"138":{"loc":{"start":{"line":111,"column":45},"end":{"line":112,"column":52}},"type":"binary-expr","locations":[{"start":{"line":111,"column":45},"end":{"line":112,"column":null}},{"start":{"line":112,"column":39},"end":{"line":112,"column":52}}]}},"s":{"0":35,"1":35,"2":35,"3":35,"4":35,"5":35,"6":35,"7":35,"8":35,"9":35,"10":35,"11":35,"12":35,"13":35,"14":35,"15":35,"16":35,"17":35,"18":35,"19":35,"20":35,"21":35,"22":35,"23":35,"24":35,"25":35,"26":35,"27":35,"28":35,"29":35,"30":35,"31":35,"32":35,"33":35,"34":35,"35":35,"36":35,"37":35,"38":35,"39":35,"40":35,"41":16,"42":16,"43":15,"44":15,"45":15,"46":4,"47":4,"48":3,"49":3,"50":3,"51":4,"52":62,"53":62,"54":1,"55":61,"56":60,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0,"64":0,"65":0,"66":0,"67":0,"68":0,"69":0,"70":4,"71":1,"72":3,"73":3,"74":3,"75":0,"76":0,"77":0,"78":0,"79":0,"80":0,"81":0,"82":0,"83":0,"84":0,"85":0,"86":61,"87":57,"88":4,"89":4,"90":2,"91":2,"92":2,"93":2,"94":1,"95":1,"96":1,"97":0,"98":1,"99":1,"100":0,"101":1,"102":2,"103":2,"104":1,"105":1,"106":0,"107":0,"108":0,"109":1,"110":1,"111":1,"112":0,"113":1,"114":0,"115":1,"116":2,"117":2,"118":2,"119":1,"120":1,"121":7,"122":7,"123":7,"124":0,"125":7,"126":7,"127":7,"128":7,"129":0,"130":7,"131":7,"132":0,"133":2,"134":2,"135":0,"136":2,"137":2,"138":2,"139":1,"140":1,"141":3,"142":3,"143":1,"144":2,"145":2,"146":2,"147":2,"148":2,"149":2,"150":2,"151":2,"152":1,"153":1,"154":0,"155":1,"156":2,"157":2,"158":1,"159":1,"160":1,"161":0,"162":0,"163":0,"164":0,"165":0,"166":0,"167":0,"168":0,"169":5,"170":5,"171":1,"172":4,"173":4,"174":0,"175":0,"176":0,"177":0,"178":0,"179":0,"180":0,"181":0,"182":0,"183":0,"184":0,"185":3,"186":3,"187":3,"188":0,"189":3,"190":3,"191":3,"192":1,"193":2,"194":2,"195":0,"196":0,"197":0,"198":0,"199":0,"200":0,"201":0,"202":0,"203":0,"204":1,"205":1,"206":0,"207":1,"208":0,"209":2,"210":2,"211":2,"212":0,"213":0,"214":0,"215":0,"216":0,"217":0,"218":0,"219":0,"220":0,"221":0,"222":0,"223":0,"224":0,"225":0,"226":0,"227":0,"228":0,"229":0,"230":0,"231":1,"232":4,"233":4,"234":4,"235":4,"236":1,"237":0,"238":0,"239":0,"240":10,"241":10,"242":10,"243":10,"244":1,"245":9,"246":9,"247":9,"248":9,"249":9,"250":2,"251":4,"252":4,"253":4,"254":4,"255":4,"256":4,"257":3,"258":1,"259":1,"260":35,"261":35},"f":{"0":35,"1":16,"2":4,"3":3,"4":3,"5":62,"6":0,"7":4,"8":61,"9":2,"10":2,"11":7,"12":0,"13":2,"14":3,"15":2,"16":1,"17":2,"18":0,"19":0,"20":0,"21":5,"22":0,"23":3,"24":0,"25":1,"26":0,"27":2,"28":0,"29":0,"30":1,"31":4,"32":1,"33":0,"34":0,"35":0,"36":10,"37":9,"38":2,"39":4,"40":4,"41":1},"b":{"0":[16,0],"1":[15,15],"2":[15,15],"3":[15,15],"4":[15,15],"5":[15,15],"6":[15,15],"7":[15,0],"8":[15,15],"9":[15,15],"10":[2,1],"11":[1,61],"12":[0,0],"13":[0,0],"14":[0,0],"15":[0,0],"16":[0,0],"17":[0,0],"18":[1,3],"19":[3,2],"20":[3,0],"21":[0,0],"22":[0,0],"23":[0,0],"24":[0,0],"25":[57,4],"26":[4,4],"27":[2,2],"28":[1,1],"29":[2,2],"30":[0,1],"31":[0,1],"32":[1,1],"33":[2,1],"34":[0,1],"35":[0,0],"36":[0,0],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[1,0],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[0,1],"50":[0,1],"51":[0,1],"52":[0,1],"53":[1,0,0],"54":[0,1],"55":[1,0,0],"56":[1,1],"57":[7,0],"58":[0,7],"59":[7,0],"60":[1,6],"61":[7,1],"62":[7,1],"63":[0,7],"64":[7,7],"65":[7,0],"66":[7,6],"67":[7,6,5],"68":[7,7,6],"69":[0,2],"70":[2,0],"71":[1,1],"72":[2,1],"73":[1,2],"74":[2,0],"75":[2,2],"76":[2,0],"77":[0,1],"78":[1,1],"79":[2,1],"80":[1,0],"81":[1,1],"82":[1,4],"83":[5,4,1,1],"84":[0,0],"85":[0,0],"86":[0,0],"87":[3],"88":[3],"89":[3],"90":[0,3],"91":[1,2],"92":[0,0],"93":[0,0],"94":[0,0],"95":[0,1],"96":[0,0],"97":[0,0],"98":[0,0],"99":[0,0],"100":[0,0],"101":[0,0],"102":[0,0],"103":[4,0],"104":[4,0],"105":[4,0],"106":[4],"107":[4],"108":[1,9],"109":[9,8],"110":[4,3],"111":[3,1],"112":[4,3],"113":[0,35],"114":[35,0],"115":[0,35],"116":[35,0],"117":[0,35],"118":[35,0],"119":[0,35],"120":[35,0],"121":[0,35],"122":[35,0],"123":[0,35],"124":[35,0],"125":[0,35],"126":[35,0],"127":[0,35],"128":[35,0],"129":[35,0],"130":[35,35],"131":[35,0],"132":[35,35],"133":[35,0],"134":[35,35],"135":[35,0],"136":[35,35],"137":[35,0],"138":[35,35]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"4":{"start":{"line":15,"column":0},"end":{"line":15,"column":56}},"5":{"start":{"line":16,"column":0},"end":{"line":16,"column":60}},"6":{"start":{"line":17,"column":0},"end":{"line":17,"column":64}},"7":{"start":{"line":20,"column":7},"end":{"line":120,"column":null}},"8":{"start":{"line":21,"column":35},"end":{"line":21,"column":52}},"9":{"start":{"line":23,"column":33},"end":{"line":23,"column":41}},"10":{"start":{"line":26,"column":24},"end":{"line":28,"column":10}},"11":{"start":{"line":30,"column":8},"end":{"line":30,"column":46}},"12":{"start":{"line":34,"column":22},"end":{"line":34,"column":44}},"13":{"start":{"line":35,"column":24},"end":{"line":35,"column":46}},"14":{"start":{"line":36,"column":24},"end":{"line":36,"column":77}},"15":{"start":{"line":37,"column":8},"end":{"line":37,"column":50}},"16":{"start":{"line":41,"column":23},"end":{"line":41,"column":77}},"17":{"start":{"line":42,"column":8},"end":{"line":42,"column":61}},"18":{"start":{"line":49,"column":26},"end":{"line":53,"column":10}},"19":{"start":{"line":55,"column":31},"end":{"line":55,"column":47}},"20":{"start":{"line":57,"column":8},"end":{"line":57,"column":44}},"21":{"start":{"line":57,"column":34},"end":{"line":57,"column":44}},"22":{"start":{"line":60,"column":22},"end":{"line":64,"column":10}},"23":{"start":{"line":67,"column":8},"end":{"line":76,"column":11}},"24":{"start":{"line":68,"column":30},"end":{"line":68,"column":46}},"25":{"start":{"line":69,"column":12},"end":{"line":75,"column":14}},"26":{"start":{"line":80,"column":8},"end":{"line":80,"column":59}},"27":{"start":{"line":91,"column":12},"end":{"line":91,"column":17}},"28":{"start":{"line":92,"column":8},"end":{"line":110,"column":10}},"29":{"start":{"line":116,"column":22},"end":{"line":116,"column":56}},"30":{"start":{"line":117,"column":26},"end":{"line":117,"column":66}},"31":{"start":{"line":118,"column":8},"end":{"line":118,"column":74}},"32":{"start":{"line":20,"column":13},"end":{"line":20,"column":29}},"33":{"start":{"line":20,"column":13},"end":{"line":120,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":23,"column":4},"end":{"line":23,"column":33}},"loc":{"start":{"line":23,"column":54},"end":{"line":23,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":25,"column":4},"end":{"line":25,"column":9}},"loc":{"start":{"line":25,"column":37},"end":{"line":31,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":33,"column":4},"end":{"line":33,"column":9}},"loc":{"start":{"line":33,"column":39},"end":{"line":38,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":40,"column":4},"end":{"line":40,"column":9}},"loc":{"start":{"line":40,"column":29},"end":{"line":43,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":45,"column":4},"end":{"line":45,"column":9}},"loc":{"start":{"line":46,"column":24},"end":{"line":77,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":67,"column":25},"end":{"line":67,"column":26}},"loc":{"start":{"line":67,"column":34},"end":{"line":76,"column":9}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":79,"column":4},"end":{"line":79,"column":9}},"loc":{"start":{"line":79,"column":31},"end":{"line":81,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":83,"column":12},"end":{"line":83,"column":22}},"loc":{"start":{"line":83,"column":47},"end":{"line":111,"column":5}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":113,"column":12},"end":{"line":113,"column":22}},"loc":{"start":{"line":114,"column":32},"end":{"line":119,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":42,"column":15},"end":{"line":42,"column":60}},"type":"cond-expr","locations":[{"start":{"line":42,"column":24},"end":{"line":42,"column":53}},{"start":{"line":42,"column":56},"end":{"line":42,"column":60}}]},"1":{"loc":{"start":{"line":57,"column":8},"end":{"line":57,"column":44}},"type":"if","locations":[{"start":{"line":57,"column":8},"end":{"line":57,"column":44}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":93,"column":16},"end":{"line":93,"column":44}},"type":"cond-expr","locations":[{"start":{"line":93,"column":27},"end":{"line":93,"column":39}},{"start":{"line":93,"column":42},"end":{"line":93,"column":44}}]},"3":{"loc":{"start":{"line":94,"column":16},"end":{"line":94,"column":48}},"type":"cond-expr","locations":[{"start":{"line":94,"column":29},"end":{"line":94,"column":43}},{"start":{"line":94,"column":46},"end":{"line":94,"column":48}}]},"4":{"loc":{"start":{"line":95,"column":16},"end":{"line":100,"column":20}},"type":"cond-expr","locations":[{"start":{"line":96,"column":18},"end":{"line":99,"column":null}},{"start":{"line":100,"column":18},"end":{"line":100,"column":20}}]},"5":{"loc":{"start":{"line":101,"column":16},"end":{"line":105,"column":20}},"type":"cond-expr","locations":[{"start":{"line":102,"column":18},"end":{"line":104,"column":null}},{"start":{"line":105,"column":18},"end":{"line":105,"column":20}}]},"6":{"loc":{"start":{"line":106,"column":16},"end":{"line":106,"column":56}},"type":"cond-expr","locations":[{"start":{"line":106,"column":33},"end":{"line":106,"column":51}},{"start":{"line":106,"column":54},"end":{"line":106,"column":56}}]},"7":{"loc":{"start":{"line":107,"column":16},"end":{"line":109,"column":20}},"type":"cond-expr","locations":[{"start":{"line":108,"column":18},"end":{"line":108,"column":77}},{"start":{"line":109,"column":18},"end":{"line":109,"column":20}}]},"8":{"loc":{"start":{"line":116,"column":22},"end":{"line":116,"column":56}},"type":"binary-expr","locations":[{"start":{"line":116,"column":22},"end":{"line":116,"column":34}},{"start":{"line":116,"column":38},"end":{"line":116,"column":56}}]},"9":{"loc":{"start":{"line":117,"column":26},"end":{"line":117,"column":66}},"type":"binary-expr","locations":[{"start":{"line":117,"column":26},"end":{"line":117,"column":45}},{"start":{"line":117,"column":49},"end":{"line":117,"column":66}}]},"10":{"loc":{"start":{"line":23,"column":41},"end":{"line":23,"column":57}},"type":"cond-expr","locations":[{"start":{"line":23,"column":54},"end":{"line":23,"column":57}},{"start":{"line":23,"column":54},"end":{"line":23,"column":57}}]},"11":{"loc":{"start":{"line":23,"column":41},"end":{"line":23,"column":54}},"type":"binary-expr","locations":[{"start":{"line":23,"column":41},"end":{"line":23,"column":57}},{"start":{"line":23,"column":41},"end":{"line":23,"column":54}}]}},"s":{"0":35,"1":35,"2":35,"3":35,"4":35,"5":35,"6":35,"7":35,"8":35,"9":35,"10":24,"11":24,"12":14,"13":14,"14":14,"15":14,"16":2,"17":2,"18":6,"19":6,"20":6,"21":2,"22":4,"23":4,"24":5,"25":5,"26":1,"27":14,"28":14,"29":14,"30":14,"31":14,"32":35,"33":35},"f":{"0":35,"1":24,"2":14,"3":2,"4":6,"5":5,"6":1,"7":14,"8":14},"b":{"0":[1,1],"1":[2,4],"2":[12,2],"3":[0,14],"4":[1,13],"5":[7,7],"6":[0,14],"7":[0,14],"8":[14,13],"9":[14,13],"10":[35,0],"11":[35,35]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"4":{"start":{"line":15,"column":0},"end":{"line":15,"column":88}},"5":{"start":{"line":16,"column":0},"end":{"line":16,"column":69}},"6":{"start":{"line":19,"column":7},"end":{"line":146,"column":null}},"7":{"start":{"line":21,"column":8},"end":{"line":21,"column":42}},"8":{"start":{"line":23,"column":33},"end":{"line":23,"column":41}},"9":{"start":{"line":28,"column":32},"end":{"line":28,"column":79}},"10":{"start":{"line":29,"column":22},"end":{"line":44,"column":10}},"11":{"start":{"line":46,"column":8},"end":{"line":46,"column":59}},"12":{"start":{"line":50,"column":21},"end":{"line":52,"column":10}},"13":{"start":{"line":53,"column":8},"end":{"line":53,"column":72}},"14":{"start":{"line":59,"column":22},"end":{"line":59,"column":44}},"15":{"start":{"line":60,"column":24},"end":{"line":60,"column":46}},"16":{"start":{"line":62,"column":22},"end":{"line":65,"column":10}},"17":{"start":{"line":66,"column":8},"end":{"line":66,"column":63}},"18":{"start":{"line":73,"column":24},"end":{"line":76,"column":10}},"19":{"start":{"line":78,"column":8},"end":{"line":78,"column":61}},"20":{"start":{"line":82,"column":8},"end":{"line":82,"column":74}},"21":{"start":{"line":88,"column":22},"end":{"line":90,"column":10}},"22":{"start":{"line":91,"column":8},"end":{"line":91,"column":63}},"23":{"start":{"line":105,"column":12},"end":{"line":105,"column":17}},"24":{"start":{"line":106,"column":8},"end":{"line":136,"column":10}},"25":{"start":{"line":142,"column":22},"end":{"line":142,"column":71}},"26":{"start":{"line":143,"column":26},"end":{"line":143,"column":66}},"27":{"start":{"line":144,"column":8},"end":{"line":144,"column":74}},"28":{"start":{"line":19,"column":13},"end":{"line":19,"column":44}},"29":{"start":{"line":19,"column":13},"end":{"line":146,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":23,"column":4},"end":{"line":23,"column":33}},"loc":{"start":{"line":23,"column":54},"end":{"line":23,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":25,"column":4},"end":{"line":25,"column":9}},"loc":{"start":{"line":26,"column":46},"end":{"line":47,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":49,"column":4},"end":{"line":49,"column":9}},"loc":{"start":{"line":49,"column":29},"end":{"line":54,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":56,"column":4},"end":{"line":56,"column":9}},"loc":{"start":{"line":57,"column":47},"end":{"line":67,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":69,"column":4},"end":{"line":69,"column":9}},"loc":{"start":{"line":71,"column":49},"end":{"line":79,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":81,"column":4},"end":{"line":81,"column":9}},"loc":{"start":{"line":81,"column":31},"end":{"line":83,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":85,"column":4},"end":{"line":85,"column":9}},"loc":{"start":{"line":86,"column":23},"end":{"line":92,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":94,"column":12},"end":{"line":94,"column":22}},"loc":{"start":{"line":95,"column":47},"end":{"line":137,"column":5}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":139,"column":12},"end":{"line":139,"column":22}},"loc":{"start":{"line":140,"column":47},"end":{"line":145,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":53,"column":15},"end":{"line":53,"column":71}},"type":"cond-expr","locations":[{"start":{"line":53,"column":22},"end":{"line":53,"column":64}},{"start":{"line":53,"column":67},"end":{"line":53,"column":71}}]},"1":{"loc":{"start":{"line":107,"column":16},"end":{"line":107,"column":42}},"type":"cond-expr","locations":[{"start":{"line":107,"column":26},"end":{"line":107,"column":37}},{"start":{"line":107,"column":40},"end":{"line":107,"column":42}}]},"2":{"loc":{"start":{"line":108,"column":16},"end":{"line":108,"column":46}},"type":"cond-expr","locations":[{"start":{"line":108,"column":28},"end":{"line":108,"column":41}},{"start":{"line":108,"column":44},"end":{"line":108,"column":46}}]},"3":{"loc":{"start":{"line":109,"column":16},"end":{"line":109,"column":56}},"type":"cond-expr","locations":[{"start":{"line":109,"column":33},"end":{"line":109,"column":51}},{"start":{"line":109,"column":54},"end":{"line":109,"column":56}}]},"4":{"loc":{"start":{"line":110,"column":16},"end":{"line":117,"column":20}},"type":"cond-expr","locations":[{"start":{"line":111,"column":18},"end":{"line":116,"column":null}},{"start":{"line":117,"column":18},"end":{"line":117,"column":20}}]},"5":{"loc":{"start":{"line":118,"column":16},"end":{"line":123,"column":20}},"type":"cond-expr","locations":[{"start":{"line":119,"column":18},"end":{"line":122,"column":null}},{"start":{"line":123,"column":18},"end":{"line":123,"column":20}}]},"6":{"loc":{"start":{"line":124,"column":16},"end":{"line":129,"column":20}},"type":"cond-expr","locations":[{"start":{"line":125,"column":18},"end":{"line":128,"column":null}},{"start":{"line":129,"column":18},"end":{"line":129,"column":20}}]},"7":{"loc":{"start":{"line":130,"column":16},"end":{"line":135,"column":20}},"type":"cond-expr","locations":[{"start":{"line":131,"column":18},"end":{"line":134,"column":null}},{"start":{"line":135,"column":18},"end":{"line":135,"column":20}}]},"8":{"loc":{"start":{"line":142,"column":22},"end":{"line":142,"column":71}},"type":"binary-expr","locations":[{"start":{"line":142,"column":22},"end":{"line":142,"column":34}},{"start":{"line":142,"column":38},"end":{"line":142,"column":71}}]},"9":{"loc":{"start":{"line":143,"column":26},"end":{"line":143,"column":66}},"type":"binary-expr","locations":[{"start":{"line":143,"column":26},"end":{"line":143,"column":45}},{"start":{"line":143,"column":49},"end":{"line":143,"column":66}}]},"10":{"loc":{"start":{"line":23,"column":41},"end":{"line":23,"column":57}},"type":"cond-expr","locations":[{"start":{"line":23,"column":54},"end":{"line":23,"column":57}},{"start":{"line":23,"column":54},"end":{"line":23,"column":57}}]},"11":{"loc":{"start":{"line":23,"column":41},"end":{"line":23,"column":54}},"type":"binary-expr","locations":[{"start":{"line":23,"column":41},"end":{"line":23,"column":57}},{"start":{"line":23,"column":41},"end":{"line":23,"column":54}}]}},"s":{"0":35,"1":35,"2":35,"3":35,"4":35,"5":35,"6":35,"7":35,"8":35,"9":20,"10":20,"11":20,"12":8,"13":8,"14":3,"15":3,"16":3,"17":3,"18":2,"19":2,"20":2,"21":7,"22":7,"23":3,"24":3,"25":3,"26":3,"27":3,"28":35,"29":35},"f":{"0":35,"1":20,"2":8,"3":3,"4":2,"5":2,"6":7,"7":3,"8":3},"b":{"0":[5,3],"1":[2,1],"2":[1,2],"3":[0,3],"4":[0,3],"5":[0,3],"6":[0,3],"7":[0,3],"8":[3,3],"9":[3,3],"10":[35,0],"11":[35,35]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":63}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":60}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":null}},"4":{"start":{"line":15,"column":0},"end":{"line":15,"column":69}},"5":{"start":{"line":18,"column":7},"end":{"line":108,"column":null}},"6":{"start":{"line":19,"column":42},"end":{"line":19,"column":66}},"7":{"start":{"line":21,"column":33},"end":{"line":21,"column":41}},"8":{"start":{"line":24,"column":28},"end":{"line":24,"column":62}},"9":{"start":{"line":25,"column":22},"end":{"line":39,"column":10}},"10":{"start":{"line":41,"column":8},"end":{"line":41,"column":50}},"11":{"start":{"line":45,"column":22},"end":{"line":45,"column":44}},"12":{"start":{"line":46,"column":24},"end":{"line":46,"column":46}},"13":{"start":{"line":47,"column":23},"end":{"line":50,"column":10}},"14":{"start":{"line":51,"column":8},"end":{"line":51,"column":55}},"15":{"start":{"line":55,"column":22},"end":{"line":61,"column":10}},"16":{"start":{"line":62,"column":8},"end":{"line":62,"column":75}},"17":{"start":{"line":62,"column":20},"end":{"line":62,"column":75}},"18":{"start":{"line":63,"column":8},"end":{"line":63,"column":63}},"19":{"start":{"line":67,"column":8},"end":{"line":67,"column":65}},"20":{"start":{"line":73,"column":23},"end":{"line":80,"column":10}},"21":{"start":{"line":81,"column":8},"end":{"line":81,"column":78}},"22":{"start":{"line":81,"column":33},"end":{"line":81,"column":76}},"23":{"start":{"line":88,"column":12},"end":{"line":88,"column":17}},"24":{"start":{"line":89,"column":8},"end":{"line":98,"column":10}},"25":{"start":{"line":104,"column":22},"end":{"line":104,"column":62}},"26":{"start":{"line":105,"column":26},"end":{"line":105,"column":66}},"27":{"start":{"line":106,"column":8},"end":{"line":106,"column":74}},"28":{"start":{"line":18,"column":13},"end":{"line":18,"column":35}},"29":{"start":{"line":18,"column":13},"end":{"line":108,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":33}},"loc":{"start":{"line":21,"column":54},"end":{"line":21,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":23,"column":4},"end":{"line":23,"column":9}},"loc":{"start":{"line":23,"column":42},"end":{"line":42,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":44,"column":4},"end":{"line":44,"column":9}},"loc":{"start":{"line":44,"column":45},"end":{"line":52,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":54,"column":4},"end":{"line":54,"column":9}},"loc":{"start":{"line":54,"column":28},"end":{"line":64,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":66,"column":4},"end":{"line":66,"column":9}},"loc":{"start":{"line":66,"column":31},"end":{"line":68,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":70,"column":4},"end":{"line":70,"column":9}},"loc":{"start":{"line":71,"column":23},"end":{"line":82,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":81,"column":26},"end":{"line":81,"column":27}},"loc":{"start":{"line":81,"column":33},"end":{"line":81,"column":76}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":84,"column":12},"end":{"line":84,"column":22}},"loc":{"start":{"line":85,"column":38},"end":{"line":99,"column":5}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":101,"column":12},"end":{"line":101,"column":22}},"loc":{"start":{"line":102,"column":38},"end":{"line":107,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":62,"column":8},"end":{"line":62,"column":75}},"type":"if","locations":[{"start":{"line":62,"column":8},"end":{"line":62,"column":75}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":90,"column":16},"end":{"line":90,"column":42}},"type":"cond-expr","locations":[{"start":{"line":90,"column":26},"end":{"line":90,"column":37}},{"start":{"line":90,"column":40},"end":{"line":90,"column":42}}]},"2":{"loc":{"start":{"line":91,"column":16},"end":{"line":91,"column":46}},"type":"cond-expr","locations":[{"start":{"line":91,"column":28},"end":{"line":91,"column":41}},{"start":{"line":91,"column":44},"end":{"line":91,"column":46}}]},"3":{"loc":{"start":{"line":92,"column":16},"end":{"line":92,"column":42}},"type":"cond-expr","locations":[{"start":{"line":92,"column":26},"end":{"line":92,"column":37}},{"start":{"line":92,"column":40},"end":{"line":92,"column":42}}]},"4":{"loc":{"start":{"line":93,"column":16},"end":{"line":93,"column":44}},"type":"cond-expr","locations":[{"start":{"line":93,"column":27},"end":{"line":93,"column":39}},{"start":{"line":93,"column":42},"end":{"line":93,"column":44}}]},"5":{"loc":{"start":{"line":94,"column":16},"end":{"line":96,"column":20}},"type":"cond-expr","locations":[{"start":{"line":95,"column":18},"end":{"line":95,"column":75}},{"start":{"line":96,"column":18},"end":{"line":96,"column":20}}]},"6":{"loc":{"start":{"line":97,"column":16},"end":{"line":97,"column":50}},"type":"cond-expr","locations":[{"start":{"line":97,"column":30},"end":{"line":97,"column":45}},{"start":{"line":97,"column":48},"end":{"line":97,"column":50}}]},"7":{"loc":{"start":{"line":104,"column":22},"end":{"line":104,"column":62}},"type":"binary-expr","locations":[{"start":{"line":104,"column":22},"end":{"line":104,"column":34}},{"start":{"line":104,"column":38},"end":{"line":104,"column":62}}]},"8":{"loc":{"start":{"line":105,"column":26},"end":{"line":105,"column":66}},"type":"binary-expr","locations":[{"start":{"line":105,"column":26},"end":{"line":105,"column":45}},{"start":{"line":105,"column":49},"end":{"line":105,"column":66}}]},"9":{"loc":{"start":{"line":21,"column":41},"end":{"line":21,"column":57}},"type":"cond-expr","locations":[{"start":{"line":21,"column":54},"end":{"line":21,"column":57}},{"start":{"line":21,"column":54},"end":{"line":21,"column":57}}]},"10":{"loc":{"start":{"line":21,"column":41},"end":{"line":21,"column":54}},"type":"binary-expr","locations":[{"start":{"line":21,"column":41},"end":{"line":21,"column":57}},{"start":{"line":21,"column":41},"end":{"line":21,"column":54}}]}},"s":{"0":35,"1":35,"2":35,"3":35,"4":35,"5":35,"6":35,"7":35,"8":12,"9":12,"10":12,"11":9,"12":9,"13":9,"14":9,"15":2,"16":2,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"23":9,"24":9,"25":9,"26":9,"27":9,"28":35,"29":35},"f":{"0":35,"1":12,"2":9,"3":2,"4":1,"5":1,"6":1,"7":9,"8":9},"b":{"0":[1,1],"1":[8,1],"2":[1,8],"3":[0,9],"4":[0,9],"5":[0,9],"6":[0,9],"7":[9,9],"8":[9,9],"9":[35,0],"10":[35,35]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":44}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":80}},"4":{"start":{"line":11,"column":7},"end":{"line":39,"column":null}},"5":{"start":{"line":12,"column":48},"end":{"line":12,"column":78}},"6":{"start":{"line":14,"column":33},"end":{"line":14,"column":41}},"7":{"start":{"line":19,"column":24},"end":{"line":21,"column":10}},"8":{"start":{"line":22,"column":8},"end":{"line":22,"column":57}},"9":{"start":{"line":26,"column":22},"end":{"line":29,"column":10}},"10":{"start":{"line":30,"column":8},"end":{"line":30,"column":59}},"11":{"start":{"line":34,"column":21},"end":{"line":36,"column":10}},"12":{"start":{"line":37,"column":8},"end":{"line":37,"column":68}},"13":{"start":{"line":11,"column":13},"end":{"line":11,"column":40}},"14":{"start":{"line":11,"column":13},"end":{"line":39,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":14,"column":4},"end":{"line":14,"column":33}},"loc":{"start":{"line":14,"column":54},"end":{"line":14,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":9}},"loc":{"start":{"line":17,"column":40},"end":{"line":23,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":25,"column":4},"end":{"line":25,"column":9}},"loc":{"start":{"line":25,"column":39},"end":{"line":31,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":33,"column":4},"end":{"line":33,"column":9}},"loc":{"start":{"line":33,"column":29},"end":{"line":38,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":37,"column":15},"end":{"line":37,"column":67}},"type":"cond-expr","locations":[{"start":{"line":37,"column":22},"end":{"line":37,"column":60}},{"start":{"line":37,"column":63},"end":{"line":37,"column":67}}]},"1":{"loc":{"start":{"line":14,"column":41},"end":{"line":14,"column":57}},"type":"cond-expr","locations":[{"start":{"line":14,"column":54},"end":{"line":14,"column":57}},{"start":{"line":14,"column":54},"end":{"line":14,"column":57}}]},"2":{"loc":{"start":{"line":14,"column":41},"end":{"line":14,"column":54}},"type":"binary-expr","locations":[{"start":{"line":14,"column":41},"end":{"line":14,"column":57}},{"start":{"line":14,"column":41},"end":{"line":14,"column":54}}]}},"s":{"0":38,"1":38,"2":38,"3":38,"4":38,"5":37,"6":37,"7":17,"8":17,"9":4,"10":4,"11":2,"12":2,"13":38,"14":38},"f":{"0":37,"1":17,"2":4,"3":2},"b":{"0":[1,1],"1":[38,0],"2":[38,38]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"4":{"start":{"line":15,"column":0},"end":{"line":15,"column":54}},"5":{"start":{"line":18,"column":7},"end":{"line":129,"column":null}},"6":{"start":{"line":19,"column":34},"end":{"line":19,"column":50}},"7":{"start":{"line":21,"column":33},"end":{"line":21,"column":41}},"8":{"start":{"line":24,"column":24},"end":{"line":26,"column":10}},"9":{"start":{"line":28,"column":8},"end":{"line":28,"column":45}},"10":{"start":{"line":32,"column":22},"end":{"line":32,"column":75}},"11":{"start":{"line":33,"column":8},"end":{"line":33,"column":58}},"12":{"start":{"line":37,"column":22},"end":{"line":37,"column":44}},"13":{"start":{"line":38,"column":24},"end":{"line":38,"column":46}},"14":{"start":{"line":40,"column":22},"end":{"line":43,"column":10}},"15":{"start":{"line":45,"column":8},"end":{"line":45,"column":47}},"16":{"start":{"line":52,"column":24},"end":{"line":62,"column":10}},"17":{"start":{"line":63,"column":8},"end":{"line":63,"column":45}},"18":{"start":{"line":67,"column":8},"end":{"line":67,"column":58}},"19":{"start":{"line":84,"column":12},"end":{"line":84,"column":17}},"20":{"start":{"line":85,"column":8},"end":{"line":119,"column":10}},"21":{"start":{"line":125,"column":22},"end":{"line":125,"column":55}},"22":{"start":{"line":126,"column":26},"end":{"line":126,"column":66}},"23":{"start":{"line":127,"column":8},"end":{"line":127,"column":74}},"24":{"start":{"line":18,"column":13},"end":{"line":18,"column":28}},"25":{"start":{"line":18,"column":13},"end":{"line":129,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":33}},"loc":{"start":{"line":21,"column":54},"end":{"line":21,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":23,"column":4},"end":{"line":23,"column":9}},"loc":{"start":{"line":23,"column":35},"end":{"line":29,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":31,"column":4},"end":{"line":31,"column":9}},"loc":{"start":{"line":31,"column":29},"end":{"line":34,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":36,"column":4},"end":{"line":36,"column":9}},"loc":{"start":{"line":36,"column":40},"end":{"line":46,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":48,"column":4},"end":{"line":48,"column":9}},"loc":{"start":{"line":50,"column":33},"end":{"line":64,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":66,"column":4},"end":{"line":66,"column":9}},"loc":{"start":{"line":66,"column":31},"end":{"line":68,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":70,"column":12},"end":{"line":70,"column":22}},"loc":{"start":{"line":70,"column":46},"end":{"line":120,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":122,"column":12},"end":{"line":122,"column":22}},"loc":{"start":{"line":123,"column":31},"end":{"line":128,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":33,"column":15},"end":{"line":33,"column":57}},"type":"cond-expr","locations":[{"start":{"line":33,"column":23},"end":{"line":33,"column":50}},{"start":{"line":33,"column":53},"end":{"line":33,"column":57}}]},"1":{"loc":{"start":{"line":56,"column":20},"end":{"line":60,"column":24}},"type":"cond-expr","locations":[{"start":{"line":57,"column":22},"end":{"line":59,"column":null}},{"start":{"line":60,"column":22},"end":{"line":60,"column":24}}]},"2":{"loc":{"start":{"line":86,"column":16},"end":{"line":88,"column":20}},"type":"cond-expr","locations":[{"start":{"line":87,"column":18},"end":{"line":87,"column":69}},{"start":{"line":88,"column":18},"end":{"line":88,"column":20}}]},"3":{"loc":{"start":{"line":89,"column":16},"end":{"line":96,"column":20}},"type":"cond-expr","locations":[{"start":{"line":90,"column":18},"end":{"line":95,"column":null}},{"start":{"line":96,"column":18},"end":{"line":96,"column":20}}]},"4":{"loc":{"start":{"line":97,"column":16},"end":{"line":109,"column":20}},"type":"cond-expr","locations":[{"start":{"line":98,"column":18},"end":{"line":108,"column":null}},{"start":{"line":109,"column":18},"end":{"line":109,"column":20}}]},"5":{"loc":{"start":{"line":110,"column":16},"end":{"line":110,"column":36}},"type":"cond-expr","locations":[{"start":{"line":110,"column":23},"end":{"line":110,"column":31}},{"start":{"line":110,"column":34},"end":{"line":110,"column":36}}]},"6":{"loc":{"start":{"line":111,"column":16},"end":{"line":111,"column":74}},"type":"cond-expr","locations":[{"start":{"line":111,"column":42},"end":{"line":111,"column":69}},{"start":{"line":111,"column":72},"end":{"line":111,"column":74}}]},"7":{"loc":{"start":{"line":112,"column":16},"end":{"line":112,"column":72}},"type":"cond-expr","locations":[{"start":{"line":112,"column":24},"end":{"line":112,"column":67}},{"start":{"line":112,"column":70},"end":{"line":112,"column":72}}]},"8":{"loc":{"start":{"line":113,"column":16},"end":{"line":113,"column":58}},"type":"cond-expr","locations":[{"start":{"line":113,"column":41},"end":{"line":113,"column":53}},{"start":{"line":113,"column":56},"end":{"line":113,"column":58}}]},"9":{"loc":{"start":{"line":114,"column":16},"end":{"line":114,"column":46}},"type":"cond-expr","locations":[{"start":{"line":114,"column":28},"end":{"line":114,"column":41}},{"start":{"line":114,"column":44},"end":{"line":114,"column":46}}]},"10":{"loc":{"start":{"line":115,"column":16},"end":{"line":115,"column":56}},"type":"cond-expr","locations":[{"start":{"line":115,"column":33},"end":{"line":115,"column":51}},{"start":{"line":115,"column":54},"end":{"line":115,"column":56}}]},"11":{"loc":{"start":{"line":116,"column":16},"end":{"line":116,"column":60}},"type":"cond-expr","locations":[{"start":{"line":116,"column":35},"end":{"line":116,"column":55}},{"start":{"line":116,"column":58},"end":{"line":116,"column":60}}]},"12":{"loc":{"start":{"line":117,"column":16},"end":{"line":117,"column":60}},"type":"cond-expr","locations":[{"start":{"line":117,"column":35},"end":{"line":117,"column":55}},{"start":{"line":117,"column":58},"end":{"line":117,"column":60}}]},"13":{"loc":{"start":{"line":118,"column":16},"end":{"line":118,"column":42}},"type":"cond-expr","locations":[{"start":{"line":118,"column":26},"end":{"line":118,"column":37}},{"start":{"line":118,"column":40},"end":{"line":118,"column":42}}]},"14":{"loc":{"start":{"line":125,"column":22},"end":{"line":125,"column":55}},"type":"binary-expr","locations":[{"start":{"line":125,"column":22},"end":{"line":125,"column":34}},{"start":{"line":125,"column":38},"end":{"line":125,"column":55}}]},"15":{"loc":{"start":{"line":126,"column":26},"end":{"line":126,"column":66}},"type":"binary-expr","locations":[{"start":{"line":126,"column":26},"end":{"line":126,"column":45}},{"start":{"line":126,"column":49},"end":{"line":126,"column":66}}]},"16":{"loc":{"start":{"line":21,"column":41},"end":{"line":21,"column":57}},"type":"cond-expr","locations":[{"start":{"line":21,"column":54},"end":{"line":21,"column":57}},{"start":{"line":21,"column":54},"end":{"line":21,"column":57}}]},"17":{"loc":{"start":{"line":21,"column":41},"end":{"line":21,"column":54}},"type":"binary-expr","locations":[{"start":{"line":21,"column":41},"end":{"line":21,"column":57}},{"start":{"line":21,"column":41},"end":{"line":21,"column":54}}]}},"s":{"0":38,"1":38,"2":38,"3":38,"4":38,"5":38,"6":38,"7":38,"8":308,"9":308,"10":163,"11":163,"12":12,"13":12,"14":12,"15":12,"16":7,"17":7,"18":2,"19":12,"20":12,"21":12,"22":12,"23":12,"24":38,"25":38},"f":{"0":38,"1":308,"2":163,"3":12,"4":7,"5":2,"6":12,"7":12},"b":{"0":[152,11],"1":[2,5],"2":[1,11],"3":[0,12],"4":[0,12],"5":[0,12],"6":[0,12],"7":[3,9],"8":[7,5],"9":[0,12],"10":[0,12],"11":[0,12],"12":[0,12],"13":[0,12],"14":[12,11],"15":[12,11],"16":[38,0],"17":[38,38]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/question.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/question.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":44}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":60}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":null}},"4":{"start":{"line":14,"column":0},"end":{"line":14,"column":60}},"5":{"start":{"line":17,"column":7},"end":{"line":89,"column":null}},"6":{"start":{"line":18,"column":37},"end":{"line":18,"column":56}},"7":{"start":{"line":20,"column":33},"end":{"line":20,"column":41}},"8":{"start":{"line":23,"column":24},"end":{"line":25,"column":10}},"9":{"start":{"line":27,"column":8},"end":{"line":27,"column":48}},"10":{"start":{"line":31,"column":25},"end":{"line":33,"column":10}},"11":{"start":{"line":34,"column":8},"end":{"line":34,"column":67}},"12":{"start":{"line":38,"column":22},"end":{"line":38,"column":44}},"13":{"start":{"line":39,"column":24},"end":{"line":39,"column":46}},"14":{"start":{"line":40,"column":22},"end":{"line":40,"column":77}},"15":{"start":{"line":41,"column":8},"end":{"line":41,"column":50}},"16":{"start":{"line":45,"column":8},"end":{"line":45,"column":61}},"17":{"start":{"line":56,"column":12},"end":{"line":56,"column":17}},"18":{"start":{"line":57,"column":8},"end":{"line":79,"column":10}},"19":{"start":{"line":85,"column":22},"end":{"line":85,"column":58}},"20":{"start":{"line":86,"column":26},"end":{"line":86,"column":66}},"21":{"start":{"line":87,"column":8},"end":{"line":87,"column":74}},"22":{"start":{"line":17,"column":13},"end":{"line":17,"column":31}},"23":{"start":{"line":17,"column":13},"end":{"line":89,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":20,"column":4},"end":{"line":20,"column":33}},"loc":{"start":{"line":20,"column":54},"end":{"line":20,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":22,"column":4},"end":{"line":22,"column":9}},"loc":{"start":{"line":22,"column":41},"end":{"line":28,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":30,"column":4},"end":{"line":30,"column":9}},"loc":{"start":{"line":30,"column":29},"end":{"line":35,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":37,"column":4},"end":{"line":37,"column":9}},"loc":{"start":{"line":37,"column":43},"end":{"line":42,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":44,"column":4},"end":{"line":44,"column":9}},"loc":{"start":{"line":44,"column":31},"end":{"line":46,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":48,"column":12},"end":{"line":48,"column":22}},"loc":{"start":{"line":48,"column":49},"end":{"line":80,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":82,"column":12},"end":{"line":82,"column":22}},"loc":{"start":{"line":83,"column":34},"end":{"line":88,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":34,"column":15},"end":{"line":34,"column":66}},"type":"cond-expr","locations":[{"start":{"line":34,"column":26},"end":{"line":34,"column":59}},{"start":{"line":34,"column":62},"end":{"line":34,"column":66}}]},"1":{"loc":{"start":{"line":58,"column":16},"end":{"line":58,"column":42}},"type":"cond-expr","locations":[{"start":{"line":58,"column":26},"end":{"line":58,"column":37}},{"start":{"line":58,"column":40},"end":{"line":58,"column":42}}]},"2":{"loc":{"start":{"line":59,"column":16},"end":{"line":59,"column":64}},"type":"cond-expr","locations":[{"start":{"line":59,"column":37},"end":{"line":59,"column":59}},{"start":{"line":59,"column":62},"end":{"line":59,"column":64}}]},"3":{"loc":{"start":{"line":60,"column":16},"end":{"line":62,"column":20}},"type":"cond-expr","locations":[{"start":{"line":61,"column":18},"end":{"line":61,"column":69}},{"start":{"line":62,"column":18},"end":{"line":62,"column":20}}]},"4":{"loc":{"start":{"line":63,"column":16},"end":{"line":67,"column":20}},"type":"cond-expr","locations":[{"start":{"line":64,"column":18},"end":{"line":66,"column":null}},{"start":{"line":67,"column":18},"end":{"line":67,"column":20}}]},"5":{"loc":{"start":{"line":68,"column":16},"end":{"line":68,"column":52}},"type":"cond-expr","locations":[{"start":{"line":68,"column":31},"end":{"line":68,"column":47}},{"start":{"line":68,"column":50},"end":{"line":68,"column":52}}]},"6":{"loc":{"start":{"line":69,"column":16},"end":{"line":78,"column":20}},"type":"cond-expr","locations":[{"start":{"line":70,"column":18},"end":{"line":77,"column":null}},{"start":{"line":78,"column":18},"end":{"line":78,"column":20}}]},"7":{"loc":{"start":{"line":70,"column":18},"end":{"line":77,"column":null}},"type":"cond-expr","locations":[{"start":{"line":71,"column":22},"end":{"line":71,"column":51}},{"start":{"line":72,"column":22},"end":{"line":77,"column":null}}]},"8":{"loc":{"start":{"line":85,"column":22},"end":{"line":85,"column":58}},"type":"binary-expr","locations":[{"start":{"line":85,"column":22},"end":{"line":85,"column":34}},{"start":{"line":85,"column":38},"end":{"line":85,"column":58}}]},"9":{"loc":{"start":{"line":86,"column":26},"end":{"line":86,"column":66}},"type":"binary-expr","locations":[{"start":{"line":86,"column":26},"end":{"line":86,"column":45}},{"start":{"line":86,"column":49},"end":{"line":86,"column":66}}]},"10":{"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":57}},"type":"cond-expr","locations":[{"start":{"line":20,"column":54},"end":{"line":20,"column":57}},{"start":{"line":20,"column":54},"end":{"line":20,"column":57}}]},"11":{"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":54}},"type":"binary-expr","locations":[{"start":{"line":20,"column":41},"end":{"line":20,"column":57}},{"start":{"line":20,"column":41},"end":{"line":20,"column":54}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":44,"7":44,"8":66,"9":66,"10":6,"11":6,"12":10,"13":10,"14":10,"15":10,"16":2,"17":10,"18":10,"19":10,"20":10,"21":10,"22":44,"23":44},"f":{"0":44,"1":66,"2":6,"3":10,"4":2,"5":10,"6":10},"b":{"0":[5,1],"1":[3,7],"2":[4,6],"3":[1,9],"4":[1,9],"5":[0,10],"6":[1,9],"7":[1,0],"8":[10,9],"9":[10,9],"10":[44,0],"11":[44,44]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":63}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"4":{"start":{"line":15,"column":0},"end":{"line":15,"column":64}},"5":{"start":{"line":18,"column":7},"end":{"line":165,"column":null}},"6":{"start":{"line":19,"column":39},"end":{"line":19,"column":60}},"7":{"start":{"line":21,"column":33},"end":{"line":21,"column":41}},"8":{"start":{"line":24,"column":24},"end":{"line":26,"column":10}},"9":{"start":{"line":28,"column":8},"end":{"line":28,"column":50}},"10":{"start":{"line":32,"column":27},"end":{"line":34,"column":10}},"11":{"start":{"line":36,"column":8},"end":{"line":38,"column":9}},"12":{"start":{"line":37,"column":12},"end":{"line":37,"column":78}},"13":{"start":{"line":40,"column":8},"end":{"line":40,"column":53}},"14":{"start":{"line":47,"column":22},"end":{"line":47,"column":44}},"15":{"start":{"line":48,"column":8},"end":{"line":48,"column":34}},"16":{"start":{"line":49,"column":24},"end":{"line":49,"column":46}},"17":{"start":{"line":50,"column":26},"end":{"line":53,"column":10}},"18":{"start":{"line":55,"column":8},"end":{"line":55,"column":56}},"19":{"start":{"line":62,"column":24},"end":{"line":82,"column":10}},"20":{"start":{"line":83,"column":8},"end":{"line":83,"column":50}},"21":{"start":{"line":87,"column":8},"end":{"line":87,"column":63}},"22":{"start":{"line":108,"column":12},"end":{"line":108,"column":17}},"23":{"start":{"line":109,"column":8},"end":{"line":155,"column":10}},"24":{"start":{"line":161,"column":22},"end":{"line":161,"column":60}},"25":{"start":{"line":162,"column":26},"end":{"line":162,"column":66}},"26":{"start":{"line":163,"column":8},"end":{"line":163,"column":74}},"27":{"start":{"line":18,"column":13},"end":{"line":18,"column":33}},"28":{"start":{"line":18,"column":13},"end":{"line":165,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":33}},"loc":{"start":{"line":21,"column":54},"end":{"line":21,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":23,"column":4},"end":{"line":23,"column":9}},"loc":{"start":{"line":23,"column":45},"end":{"line":29,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":31,"column":4},"end":{"line":31,"column":9}},"loc":{"start":{"line":31,"column":28},"end":{"line":41,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":43,"column":4},"end":{"line":43,"column":9}},"loc":{"start":{"line":45,"column":36},"end":{"line":56,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":58,"column":4},"end":{"line":58,"column":9}},"loc":{"start":{"line":60,"column":38},"end":{"line":84,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":86,"column":4},"end":{"line":86,"column":9}},"loc":{"start":{"line":86,"column":31},"end":{"line":88,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":90,"column":12},"end":{"line":90,"column":22}},"loc":{"start":{"line":91,"column":36},"end":{"line":156,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":158,"column":12},"end":{"line":158,"column":22}},"loc":{"start":{"line":159,"column":36},"end":{"line":164,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":36,"column":8},"end":{"line":38,"column":9}},"type":"if","locations":[{"start":{"line":36,"column":8},"end":{"line":38,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":66,"column":20},"end":{"line":72,"column":24}},"type":"cond-expr","locations":[{"start":{"line":67,"column":22},"end":{"line":71,"column":null}},{"start":{"line":72,"column":22},"end":{"line":72,"column":24}}]},"2":{"loc":{"start":{"line":73,"column":20},"end":{"line":80,"column":24}},"type":"cond-expr","locations":[{"start":{"line":74,"column":22},"end":{"line":79,"column":null}},{"start":{"line":80,"column":22},"end":{"line":80,"column":24}}]},"3":{"loc":{"start":{"line":110,"column":16},"end":{"line":110,"column":44}},"type":"cond-expr","locations":[{"start":{"line":110,"column":27},"end":{"line":110,"column":39}},{"start":{"line":110,"column":42},"end":{"line":110,"column":44}}]},"4":{"loc":{"start":{"line":111,"column":16},"end":{"line":111,"column":52}},"type":"cond-expr","locations":[{"start":{"line":111,"column":31},"end":{"line":111,"column":47}},{"start":{"line":111,"column":50},"end":{"line":111,"column":52}}]},"5":{"loc":{"start":{"line":112,"column":16},"end":{"line":114,"column":20}},"type":"cond-expr","locations":[{"start":{"line":113,"column":18},"end":{"line":113,"column":75}},{"start":{"line":114,"column":18},"end":{"line":114,"column":20}}]},"6":{"loc":{"start":{"line":115,"column":16},"end":{"line":119,"column":20}},"type":"cond-expr","locations":[{"start":{"line":116,"column":18},"end":{"line":118,"column":null}},{"start":{"line":119,"column":18},"end":{"line":119,"column":20}}]},"7":{"loc":{"start":{"line":120,"column":16},"end":{"line":127,"column":20}},"type":"cond-expr","locations":[{"start":{"line":121,"column":18},"end":{"line":126,"column":null}},{"start":{"line":127,"column":18},"end":{"line":127,"column":20}}]},"8":{"loc":{"start":{"line":128,"column":16},"end":{"line":135,"column":20}},"type":"cond-expr","locations":[{"start":{"line":129,"column":18},"end":{"line":134,"column":null}},{"start":{"line":135,"column":18},"end":{"line":135,"column":20}}]},"9":{"loc":{"start":{"line":136,"column":16},"end":{"line":138,"column":20}},"type":"cond-expr","locations":[{"start":{"line":137,"column":18},"end":{"line":137,"column":71}},{"start":{"line":138,"column":18},"end":{"line":138,"column":20}}]},"10":{"loc":{"start":{"line":139,"column":16},"end":{"line":139,"column":48}},"type":"cond-expr","locations":[{"start":{"line":139,"column":29},"end":{"line":139,"column":43}},{"start":{"line":139,"column":46},"end":{"line":139,"column":48}}]},"11":{"loc":{"start":{"line":140,"column":16},"end":{"line":147,"column":20}},"type":"cond-expr","locations":[{"start":{"line":141,"column":18},"end":{"line":146,"column":null}},{"start":{"line":147,"column":18},"end":{"line":147,"column":20}}]},"12":{"loc":{"start":{"line":148,"column":16},"end":{"line":148,"column":40}},"type":"cond-expr","locations":[{"start":{"line":148,"column":25},"end":{"line":148,"column":35}},{"start":{"line":148,"column":38},"end":{"line":148,"column":40}}]},"13":{"loc":{"start":{"line":149,"column":16},"end":{"line":151,"column":20}},"type":"cond-expr","locations":[{"start":{"line":150,"column":18},"end":{"line":150,"column":77}},{"start":{"line":151,"column":18},"end":{"line":151,"column":20}}]},"14":{"loc":{"start":{"line":152,"column":16},"end":{"line":152,"column":46}},"type":"cond-expr","locations":[{"start":{"line":152,"column":28},"end":{"line":152,"column":41}},{"start":{"line":152,"column":44},"end":{"line":152,"column":46}}]},"15":{"loc":{"start":{"line":153,"column":16},"end":{"line":153,"column":48}},"type":"cond-expr","locations":[{"start":{"line":153,"column":29},"end":{"line":153,"column":43}},{"start":{"line":153,"column":46},"end":{"line":153,"column":48}}]},"16":{"loc":{"start":{"line":154,"column":16},"end":{"line":154,"column":50}},"type":"cond-expr","locations":[{"start":{"line":154,"column":30},"end":{"line":154,"column":45}},{"start":{"line":154,"column":48},"end":{"line":154,"column":50}}]},"17":{"loc":{"start":{"line":161,"column":22},"end":{"line":161,"column":60}},"type":"binary-expr","locations":[{"start":{"line":161,"column":22},"end":{"line":161,"column":34}},{"start":{"line":161,"column":38},"end":{"line":161,"column":60}}]},"18":{"loc":{"start":{"line":162,"column":26},"end":{"line":162,"column":66}},"type":"binary-expr","locations":[{"start":{"line":162,"column":26},"end":{"line":162,"column":45}},{"start":{"line":162,"column":49},"end":{"line":162,"column":66}}]},"19":{"loc":{"start":{"line":21,"column":41},"end":{"line":21,"column":57}},"type":"cond-expr","locations":[{"start":{"line":21,"column":54},"end":{"line":21,"column":57}},{"start":{"line":21,"column":54},"end":{"line":21,"column":57}}]},"20":{"loc":{"start":{"line":21,"column":41},"end":{"line":21,"column":54}},"type":"binary-expr","locations":[{"start":{"line":21,"column":41},"end":{"line":21,"column":57}},{"start":{"line":21,"column":41},"end":{"line":21,"column":54}}]}},"s":{"0":38,"1":38,"2":38,"3":38,"4":38,"5":38,"6":37,"7":37,"8":26,"9":26,"10":9,"11":9,"12":1,"13":8,"14":18,"15":18,"16":18,"17":18,"18":18,"19":3,"20":3,"21":1,"22":18,"23":18,"24":18,"25":18,"26":18,"27":38,"28":38},"f":{"0":37,"1":26,"2":9,"3":18,"4":3,"5":1,"6":18,"7":18},"b":{"0":[1,8],"1":[0,3],"2":[3,0],"3":[0,18],"4":[1,17],"5":[0,18],"6":[1,17],"7":[1,17],"8":[0,18],"9":[0,18],"10":[0,18],"11":[0,18],"12":[0,18],"13":[0,18],"14":[0,18],"15":[0,18],"16":[0,18],"17":[18,17],"18":[18,17],"19":[38,0],"20":[38,38]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/review-question-relation.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/review-question-relation.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":44}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":60}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":null}},"4":{"start":{"line":14,"column":0},"end":{"line":14,"column":60}},"5":{"start":{"line":15,"column":0},"end":{"line":15,"column":90}},"6":{"start":{"line":18,"column":7},"end":{"line":150,"column":null}},"7":{"start":{"line":20,"column":8},"end":{"line":20,"column":43}},"8":{"start":{"line":22,"column":33},"end":{"line":22,"column":41}},"9":{"start":{"line":27,"column":31},"end":{"line":27,"column":78}},"10":{"start":{"line":28,"column":24},"end":{"line":45,"column":10}},"11":{"start":{"line":47,"column":8},"end":{"line":47,"column":62}},"12":{"start":{"line":54,"column":22},"end":{"line":54,"column":44}},"13":{"start":{"line":55,"column":8},"end":{"line":55,"column":34}},"14":{"start":{"line":56,"column":24},"end":{"line":56,"column":46}},"15":{"start":{"line":57,"column":26},"end":{"line":60,"column":10}},"16":{"start":{"line":61,"column":8},"end":{"line":61,"column":68}},"17":{"start":{"line":65,"column":8},"end":{"line":72,"column":11}},"18":{"start":{"line":88,"column":12},"end":{"line":88,"column":17}},"19":{"start":{"line":89,"column":8},"end":{"line":140,"column":10}},"20":{"start":{"line":146,"column":22},"end":{"line":146,"column":72}},"21":{"start":{"line":147,"column":26},"end":{"line":147,"column":66}},"22":{"start":{"line":148,"column":8},"end":{"line":148,"column":74}},"23":{"start":{"line":18,"column":13},"end":{"line":18,"column":45}},"24":{"start":{"line":18,"column":13},"end":{"line":150,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":22,"column":4},"end":{"line":22,"column":33}},"loc":{"start":{"line":22,"column":54},"end":{"line":22,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":24,"column":4},"end":{"line":24,"column":9}},"loc":{"start":{"line":25,"column":46},"end":{"line":48,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":50,"column":4},"end":{"line":50,"column":9}},"loc":{"start":{"line":52,"column":48},"end":{"line":62,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":64,"column":4},"end":{"line":64,"column":9}},"loc":{"start":{"line":64,"column":53},"end":{"line":73,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":75,"column":12},"end":{"line":75,"column":22}},"loc":{"start":{"line":76,"column":48},"end":{"line":141,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":143,"column":12},"end":{"line":143,"column":22}},"loc":{"start":{"line":144,"column":48},"end":{"line":149,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":90,"column":16},"end":{"line":90,"column":44}},"type":"cond-expr","locations":[{"start":{"line":90,"column":27},"end":{"line":90,"column":39}},{"start":{"line":90,"column":42},"end":{"line":90,"column":44}}]},"1":{"loc":{"start":{"line":91,"column":16},"end":{"line":91,"column":48}},"type":"cond-expr","locations":[{"start":{"line":91,"column":29},"end":{"line":91,"column":43}},{"start":{"line":91,"column":46},"end":{"line":91,"column":48}}]},"2":{"loc":{"start":{"line":92,"column":16},"end":{"line":99,"column":20}},"type":"cond-expr","locations":[{"start":{"line":93,"column":18},"end":{"line":98,"column":null}},{"start":{"line":99,"column":18},"end":{"line":99,"column":20}}]},"3":{"loc":{"start":{"line":100,"column":16},"end":{"line":104,"column":20}},"type":"cond-expr","locations":[{"start":{"line":101,"column":18},"end":{"line":103,"column":null}},{"start":{"line":104,"column":18},"end":{"line":104,"column":20}}]},"4":{"loc":{"start":{"line":105,"column":16},"end":{"line":105,"column":52}},"type":"cond-expr","locations":[{"start":{"line":105,"column":31},"end":{"line":105,"column":47}},{"start":{"line":105,"column":50},"end":{"line":105,"column":52}}]},"5":{"loc":{"start":{"line":106,"column":16},"end":{"line":113,"column":20}},"type":"cond-expr","locations":[{"start":{"line":107,"column":18},"end":{"line":112,"column":null}},{"start":{"line":113,"column":18},"end":{"line":113,"column":20}}]},"6":{"loc":{"start":{"line":114,"column":16},"end":{"line":121,"column":20}},"type":"cond-expr","locations":[{"start":{"line":115,"column":18},"end":{"line":120,"column":null}},{"start":{"line":121,"column":18},"end":{"line":121,"column":20}}]},"7":{"loc":{"start":{"line":122,"column":16},"end":{"line":129,"column":20}},"type":"cond-expr","locations":[{"start":{"line":123,"column":18},"end":{"line":128,"column":null}},{"start":{"line":129,"column":18},"end":{"line":129,"column":20}}]},"8":{"loc":{"start":{"line":130,"column":16},"end":{"line":139,"column":20}},"type":"cond-expr","locations":[{"start":{"line":131,"column":18},"end":{"line":138,"column":null}},{"start":{"line":139,"column":18},"end":{"line":139,"column":20}}]},"9":{"loc":{"start":{"line":131,"column":18},"end":{"line":138,"column":null}},"type":"cond-expr","locations":[{"start":{"line":132,"column":22},"end":{"line":132,"column":51}},{"start":{"line":133,"column":22},"end":{"line":138,"column":null}}]},"10":{"loc":{"start":{"line":146,"column":22},"end":{"line":146,"column":72}},"type":"binary-expr","locations":[{"start":{"line":146,"column":22},"end":{"line":146,"column":34}},{"start":{"line":146,"column":38},"end":{"line":146,"column":72}}]},"11":{"loc":{"start":{"line":147,"column":26},"end":{"line":147,"column":66}},"type":"binary-expr","locations":[{"start":{"line":147,"column":26},"end":{"line":147,"column":45}},{"start":{"line":147,"column":49},"end":{"line":147,"column":66}}]},"12":{"loc":{"start":{"line":22,"column":41},"end":{"line":22,"column":57}},"type":"cond-expr","locations":[{"start":{"line":22,"column":54},"end":{"line":22,"column":57}},{"start":{"line":22,"column":54},"end":{"line":22,"column":57}}]},"13":{"loc":{"start":{"line":22,"column":41},"end":{"line":22,"column":54}},"type":"binary-expr","locations":[{"start":{"line":22,"column":41},"end":{"line":22,"column":57}},{"start":{"line":22,"column":41},"end":{"line":22,"column":54}}]}},"s":{"0":35,"1":35,"2":35,"3":35,"4":35,"5":35,"6":35,"7":35,"8":35,"9":15,"10":15,"11":15,"12":11,"13":11,"14":11,"15":11,"16":11,"17":2,"18":11,"19":11,"20":11,"21":11,"22":11,"23":35,"24":35},"f":{"0":35,"1":15,"2":11,"3":2,"4":11,"5":11},"b":{"0":[0,11],"1":[0,11],"2":[1,10],"3":[0,11],"4":[0,11],"5":[0,11],"6":[0,11],"7":[0,11],"8":[0,11],"9":[0,0],"10":[11,11],"11":[11,11],"12":[35,0],"13":[35,35]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/review-stage-history.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/review-stage-history.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":44}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":82}},"4":{"start":{"line":11,"column":7},"end":{"line":42,"column":null}},"5":{"start":{"line":13,"column":8},"end":{"line":13,"column":39}},"6":{"start":{"line":15,"column":33},"end":{"line":15,"column":41}},"7":{"start":{"line":20,"column":24},"end":{"line":22,"column":10}},"8":{"start":{"line":23,"column":8},"end":{"line":23,"column":58}},"9":{"start":{"line":29,"column":22},"end":{"line":32,"column":10}},"10":{"start":{"line":33,"column":8},"end":{"line":33,"column":60}},"11":{"start":{"line":37,"column":21},"end":{"line":39,"column":10}},"12":{"start":{"line":40,"column":8},"end":{"line":40,"column":69}},"13":{"start":{"line":11,"column":13},"end":{"line":11,"column":41}},"14":{"start":{"line":11,"column":13},"end":{"line":42,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":15,"column":4},"end":{"line":15,"column":33}},"loc":{"start":{"line":15,"column":54},"end":{"line":15,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":17,"column":4},"end":{"line":17,"column":9}},"loc":{"start":{"line":18,"column":41},"end":{"line":24,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":26,"column":4},"end":{"line":26,"column":9}},"loc":{"start":{"line":27,"column":24},"end":{"line":34,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":36,"column":4},"end":{"line":36,"column":9}},"loc":{"start":{"line":36,"column":29},"end":{"line":41,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":40,"column":15},"end":{"line":40,"column":68}},"type":"cond-expr","locations":[{"start":{"line":40,"column":22},"end":{"line":40,"column":61}},{"start":{"line":40,"column":64},"end":{"line":40,"column":68}}]},"1":{"loc":{"start":{"line":15,"column":41},"end":{"line":15,"column":57}},"type":"cond-expr","locations":[{"start":{"line":15,"column":54},"end":{"line":15,"column":57}},{"start":{"line":15,"column":54},"end":{"line":15,"column":57}}]},"2":{"loc":{"start":{"line":15,"column":41},"end":{"line":15,"column":54}},"type":"binary-expr","locations":[{"start":{"line":15,"column":41},"end":{"line":15,"column":57}},{"start":{"line":15,"column":41},"end":{"line":15,"column":54}}]}},"s":{"0":35,"1":35,"2":35,"3":35,"4":35,"5":35,"6":35,"7":13,"8":13,"9":4,"10":4,"11":2,"12":2,"13":35,"14":35},"f":{"0":35,"1":13,"2":4,"3":2},"b":{"0":[1,1],"1":[35,0],"2":[35,35]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/review.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/review.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"4":{"start":{"line":15,"column":0},"end":{"line":15,"column":56}},"5":{"start":{"line":18,"column":7},"end":{"line":165,"column":null}},"6":{"start":{"line":19,"column":35},"end":{"line":19,"column":52}},"7":{"start":{"line":21,"column":33},"end":{"line":21,"column":41}},"8":{"start":{"line":24,"column":24},"end":{"line":26,"column":10}},"9":{"start":{"line":28,"column":8},"end":{"line":28,"column":46}},"10":{"start":{"line":32,"column":23},"end":{"line":32,"column":77}},"11":{"start":{"line":33,"column":8},"end":{"line":33,"column":61}},"12":{"start":{"line":37,"column":22},"end":{"line":37,"column":44}},"13":{"start":{"line":38,"column":24},"end":{"line":38,"column":46}},"14":{"start":{"line":39,"column":22},"end":{"line":39,"column":75}},"15":{"start":{"line":40,"column":8},"end":{"line":40,"column":48}},"16":{"start":{"line":44,"column":24},"end":{"line":46,"column":10}},"17":{"start":{"line":47,"column":8},"end":{"line":47,"column":50}},"18":{"start":{"line":54,"column":24},"end":{"line":64,"column":10}},"19":{"start":{"line":65,"column":8},"end":{"line":65,"column":46}},"20":{"start":{"line":72,"column":8},"end":{"line":82,"column":11}},"21":{"start":{"line":103,"column":12},"end":{"line":103,"column":17}},"22":{"start":{"line":104,"column":8},"end":{"line":155,"column":10}},"23":{"start":{"line":161,"column":22},"end":{"line":161,"column":56}},"24":{"start":{"line":162,"column":26},"end":{"line":162,"column":66}},"25":{"start":{"line":163,"column":8},"end":{"line":163,"column":74}},"26":{"start":{"line":18,"column":13},"end":{"line":18,"column":29}},"27":{"start":{"line":18,"column":13},"end":{"line":165,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":33}},"loc":{"start":{"line":21,"column":54},"end":{"line":21,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":23,"column":4},"end":{"line":23,"column":9}},"loc":{"start":{"line":23,"column":37},"end":{"line":29,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":31,"column":4},"end":{"line":31,"column":9}},"loc":{"start":{"line":31,"column":29},"end":{"line":34,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":36,"column":4},"end":{"line":36,"column":9}},"loc":{"start":{"line":36,"column":41},"end":{"line":41,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":43,"column":4},"end":{"line":43,"column":9}},"loc":{"start":{"line":43,"column":39},"end":{"line":48,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":50,"column":4},"end":{"line":50,"column":9}},"loc":{"start":{"line":52,"column":34},"end":{"line":66,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":68,"column":4},"end":{"line":68,"column":9}},"loc":{"start":{"line":68,"column":31},"end":{"line":83,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":85,"column":12},"end":{"line":85,"column":22}},"loc":{"start":{"line":85,"column":47},"end":{"line":156,"column":5}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":158,"column":12},"end":{"line":158,"column":22}},"loc":{"start":{"line":159,"column":32},"end":{"line":164,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":33,"column":15},"end":{"line":33,"column":60}},"type":"cond-expr","locations":[{"start":{"line":33,"column":24},"end":{"line":33,"column":53}},{"start":{"line":33,"column":56},"end":{"line":33,"column":60}}]},"1":{"loc":{"start":{"line":58,"column":20},"end":{"line":62,"column":24}},"type":"cond-expr","locations":[{"start":{"line":59,"column":22},"end":{"line":61,"column":null}},{"start":{"line":62,"column":22},"end":{"line":62,"column":24}}]},"2":{"loc":{"start":{"line":105,"column":16},"end":{"line":105,"column":42}},"type":"cond-expr","locations":[{"start":{"line":105,"column":26},"end":{"line":105,"column":37}},{"start":{"line":105,"column":40},"end":{"line":105,"column":42}}]},"3":{"loc":{"start":{"line":106,"column":16},"end":{"line":113,"column":20}},"type":"cond-expr","locations":[{"start":{"line":107,"column":18},"end":{"line":112,"column":null}},{"start":{"line":113,"column":18},"end":{"line":113,"column":20}}]},"4":{"loc":{"start":{"line":114,"column":16},"end":{"line":114,"column":58}},"type":"cond-expr","locations":[{"start":{"line":114,"column":34},"end":{"line":114,"column":53}},{"start":{"line":114,"column":56},"end":{"line":114,"column":58}}]},"5":{"loc":{"start":{"line":115,"column":16},"end":{"line":122,"column":20}},"type":"cond-expr","locations":[{"start":{"line":116,"column":18},"end":{"line":121,"column":null}},{"start":{"line":122,"column":18},"end":{"line":122,"column":20}}]},"6":{"loc":{"start":{"line":123,"column":16},"end":{"line":123,"column":36}},"type":"cond-expr","locations":[{"start":{"line":123,"column":23},"end":{"line":123,"column":31}},{"start":{"line":123,"column":34},"end":{"line":123,"column":36}}]},"7":{"loc":{"start":{"line":124,"column":16},"end":{"line":126,"column":20}},"type":"cond-expr","locations":[{"start":{"line":125,"column":18},"end":{"line":125,"column":79}},{"start":{"line":126,"column":18},"end":{"line":126,"column":20}}]},"8":{"loc":{"start":{"line":127,"column":16},"end":{"line":129,"column":20}},"type":"cond-expr","locations":[{"start":{"line":128,"column":18},"end":{"line":128,"column":71}},{"start":{"line":129,"column":18},"end":{"line":129,"column":20}}]},"9":{"loc":{"start":{"line":130,"column":16},"end":{"line":130,"column":40}},"type":"cond-expr","locations":[{"start":{"line":130,"column":25},"end":{"line":130,"column":35}},{"start":{"line":130,"column":38},"end":{"line":130,"column":40}}]},"10":{"loc":{"start":{"line":131,"column":16},"end":{"line":133,"column":20}},"type":"cond-expr","locations":[{"start":{"line":132,"column":18},"end":{"line":132,"column":77}},{"start":{"line":133,"column":18},"end":{"line":133,"column":20}}]},"11":{"loc":{"start":{"line":134,"column":16},"end":{"line":134,"column":46}},"type":"cond-expr","locations":[{"start":{"line":134,"column":28},"end":{"line":134,"column":41}},{"start":{"line":134,"column":44},"end":{"line":134,"column":46}}]},"12":{"loc":{"start":{"line":135,"column":16},"end":{"line":142,"column":20}},"type":"cond-expr","locations":[{"start":{"line":136,"column":18},"end":{"line":141,"column":null}},{"start":{"line":142,"column":18},"end":{"line":142,"column":20}}]},"13":{"loc":{"start":{"line":143,"column":16},"end":{"line":143,"column":62}},"type":"cond-expr","locations":[{"start":{"line":143,"column":36},"end":{"line":143,"column":57}},{"start":{"line":143,"column":60},"end":{"line":143,"column":62}}]},"14":{"loc":{"start":{"line":144,"column":16},"end":{"line":151,"column":20}},"type":"cond-expr","locations":[{"start":{"line":145,"column":18},"end":{"line":150,"column":null}},{"start":{"line":151,"column":18},"end":{"line":151,"column":20}}]},"15":{"loc":{"start":{"line":152,"column":16},"end":{"line":152,"column":42}},"type":"cond-expr","locations":[{"start":{"line":152,"column":26},"end":{"line":152,"column":37}},{"start":{"line":152,"column":40},"end":{"line":152,"column":42}}]},"16":{"loc":{"start":{"line":153,"column":16},"end":{"line":153,"column":73}},"type":"cond-expr","locations":[{"start":{"line":153,"column":24},"end":{"line":153,"column":68}},{"start":{"line":153,"column":71},"end":{"line":153,"column":73}}]},"17":{"loc":{"start":{"line":154,"column":16},"end":{"line":154,"column":44}},"type":"cond-expr","locations":[{"start":{"line":154,"column":27},"end":{"line":154,"column":39}},{"start":{"line":154,"column":42},"end":{"line":154,"column":44}}]},"18":{"loc":{"start":{"line":161,"column":22},"end":{"line":161,"column":56}},"type":"binary-expr","locations":[{"start":{"line":161,"column":22},"end":{"line":161,"column":34}},{"start":{"line":161,"column":38},"end":{"line":161,"column":56}}]},"19":{"loc":{"start":{"line":162,"column":26},"end":{"line":162,"column":66}},"type":"binary-expr","locations":[{"start":{"line":162,"column":26},"end":{"line":162,"column":45}},{"start":{"line":162,"column":49},"end":{"line":162,"column":66}}]},"20":{"loc":{"start":{"line":21,"column":41},"end":{"line":21,"column":57}},"type":"cond-expr","locations":[{"start":{"line":21,"column":54},"end":{"line":21,"column":57}},{"start":{"line":21,"column":54},"end":{"line":21,"column":57}}]},"21":{"loc":{"start":{"line":21,"column":41},"end":{"line":21,"column":54}},"type":"binary-expr","locations":[{"start":{"line":21,"column":41},"end":{"line":21,"column":57}},{"start":{"line":21,"column":41},"end":{"line":21,"column":54}}]}},"s":{"0":38,"1":38,"2":38,"3":38,"4":38,"5":38,"6":38,"7":38,"8":222,"9":222,"10":103,"11":103,"12":10,"13":10,"14":10,"15":10,"16":23,"17":22,"18":5,"19":5,"20":2,"21":10,"22":10,"23":10,"24":10,"25":10,"26":38,"27":38},"f":{"0":38,"1":222,"2":103,"3":10,"4":23,"5":5,"6":2,"7":10,"8":10},"b":{"0":[98,5],"1":[1,4],"2":[0,10],"3":[0,10],"4":[0,10],"5":[0,10],"6":[1,9],"7":[0,10],"8":[0,10],"9":[0,10],"10":[0,10],"11":[0,10],"12":[0,10],"13":[0,10],"14":[0,10],"15":[4,6],"16":[3,7],"17":[0,10],"18":[10,9],"19":[10,9],"20":[38,0],"21":[38,38]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/reviewer.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/infrastructure/prisma-repositories/reviewer.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":44}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":60}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":null}},"4":{"start":{"line":14,"column":0},"end":{"line":14,"column":60}},"5":{"start":{"line":17,"column":7},"end":{"line":83,"column":null}},"6":{"start":{"line":18,"column":37},"end":{"line":18,"column":56}},"7":{"start":{"line":20,"column":33},"end":{"line":20,"column":41}},"8":{"start":{"line":23,"column":24},"end":{"line":25,"column":10}},"9":{"start":{"line":26,"column":8},"end":{"line":26,"column":48}},"10":{"start":{"line":33,"column":22},"end":{"line":33,"column":44}},"11":{"start":{"line":34,"column":8},"end":{"line":34,"column":34}},"12":{"start":{"line":35,"column":24},"end":{"line":35,"column":46}},"13":{"start":{"line":36,"column":22},"end":{"line":36,"column":77}},"14":{"start":{"line":37,"column":8},"end":{"line":37,"column":50}},"15":{"start":{"line":41,"column":8},"end":{"line":41,"column":61}},"16":{"start":{"line":53,"column":12},"end":{"line":53,"column":17}},"17":{"start":{"line":54,"column":8},"end":{"line":73,"column":10}},"18":{"start":{"line":79,"column":22},"end":{"line":79,"column":58}},"19":{"start":{"line":80,"column":26},"end":{"line":80,"column":66}},"20":{"start":{"line":81,"column":8},"end":{"line":81,"column":74}},"21":{"start":{"line":17,"column":13},"end":{"line":17,"column":31}},"22":{"start":{"line":17,"column":13},"end":{"line":83,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":20,"column":4},"end":{"line":20,"column":33}},"loc":{"start":{"line":20,"column":54},"end":{"line":20,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":22,"column":4},"end":{"line":22,"column":9}},"loc":{"start":{"line":22,"column":41},"end":{"line":27,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":29,"column":4},"end":{"line":29,"column":9}},"loc":{"start":{"line":31,"column":34},"end":{"line":38,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":40,"column":4},"end":{"line":40,"column":9}},"loc":{"start":{"line":40,"column":31},"end":{"line":42,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":44,"column":12},"end":{"line":44,"column":22}},"loc":{"start":{"line":44,"column":49},"end":{"line":74,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":76,"column":12},"end":{"line":76,"column":22}},"loc":{"start":{"line":77,"column":34},"end":{"line":82,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":55,"column":16},"end":{"line":55,"column":44}},"type":"cond-expr","locations":[{"start":{"line":55,"column":27},"end":{"line":55,"column":39}},{"start":{"line":55,"column":42},"end":{"line":55,"column":44}}]},"1":{"loc":{"start":{"line":56,"column":16},"end":{"line":56,"column":48}},"type":"cond-expr","locations":[{"start":{"line":56,"column":29},"end":{"line":56,"column":43}},{"start":{"line":56,"column":46},"end":{"line":56,"column":48}}]},"2":{"loc":{"start":{"line":57,"column":16},"end":{"line":59,"column":20}},"type":"cond-expr","locations":[{"start":{"line":58,"column":18},"end":{"line":58,"column":75}},{"start":{"line":59,"column":18},"end":{"line":59,"column":20}}]},"3":{"loc":{"start":{"line":60,"column":16},"end":{"line":60,"column":48}},"type":"cond-expr","locations":[{"start":{"line":60,"column":29},"end":{"line":60,"column":43}},{"start":{"line":60,"column":46},"end":{"line":60,"column":48}}]},"4":{"loc":{"start":{"line":61,"column":16},"end":{"line":68,"column":20}},"type":"cond-expr","locations":[{"start":{"line":62,"column":18},"end":{"line":67,"column":null}},{"start":{"line":68,"column":18},"end":{"line":68,"column":20}}]},"5":{"loc":{"start":{"line":69,"column":16},"end":{"line":69,"column":40}},"type":"cond-expr","locations":[{"start":{"line":69,"column":25},"end":{"line":69,"column":35}},{"start":{"line":69,"column":38},"end":{"line":69,"column":40}}]},"6":{"loc":{"start":{"line":70,"column":16},"end":{"line":72,"column":20}},"type":"cond-expr","locations":[{"start":{"line":71,"column":18},"end":{"line":71,"column":77}},{"start":{"line":72,"column":18},"end":{"line":72,"column":20}}]},"7":{"loc":{"start":{"line":79,"column":22},"end":{"line":79,"column":58}},"type":"binary-expr","locations":[{"start":{"line":79,"column":22},"end":{"line":79,"column":34}},{"start":{"line":79,"column":38},"end":{"line":79,"column":58}}]},"8":{"loc":{"start":{"line":80,"column":26},"end":{"line":80,"column":66}},"type":"binary-expr","locations":[{"start":{"line":80,"column":26},"end":{"line":80,"column":45}},{"start":{"line":80,"column":49},"end":{"line":80,"column":66}}]},"9":{"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":57}},"type":"cond-expr","locations":[{"start":{"line":20,"column":54},"end":{"line":20,"column":57}},{"start":{"line":20,"column":54},"end":{"line":20,"column":57}}]},"10":{"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":54}},"type":"binary-expr","locations":[{"start":{"line":20,"column":41},"end":{"line":20,"column":57}},{"start":{"line":20,"column":41},"end":{"line":20,"column":54}}]}},"s":{"0":38,"1":38,"2":38,"3":38,"4":38,"5":38,"6":37,"7":37,"8":9,"9":9,"10":11,"11":11,"12":11,"13":11,"14":11,"15":2,"16":11,"17":11,"18":11,"19":11,"20":11,"21":38,"22":38},"f":{"0":37,"1":9,"2":11,"3":2,"4":11,"5":11},"b":{"0":[0,11],"1":[4,7],"2":[1,10],"3":[0,11],"4":[0,11],"5":[0,11],"6":[0,11],"7":[11,10],"8":[11,10],"9":[38,0],"10":[38,38]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/services/identity-role.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/services/identity-role.service.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":52}},"1":{"start":{"line":5,"column":0},"end":{"line":5,"column":73}},"2":{"start":{"line":8,"column":7},"end":{"line":21,"column":null}},"3":{"start":{"line":11,"column":25},"end":{"line":11,"column":50}},"4":{"start":{"line":15,"column":8},"end":{"line":15,"column":35}},"5":{"start":{"line":19,"column":8},"end":{"line":19,"column":45}},"6":{"start":{"line":8,"column":13},"end":{"line":8,"column":32}},"7":{"start":{"line":8,"column":13},"end":{"line":21,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":9,"column":4},"end":{"line":9,"column":null}},"loc":{"start":{"line":11,"column":50},"end":{"line":12,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":14,"column":4},"end":{"line":14,"column":9}},"loc":{"start":{"line":14,"column":14},"end":{"line":16,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":18,"column":42},"end":{"line":20,"column":5}}}},"branchMap":{},"s":{"0":57,"1":57,"2":57,"3":57,"4":3,"5":3,"6":57,"7":57},"f":{"0":57,"1":3,"2":3},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/services/identity-user.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/services/identity-user.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":null}},"2":{"start":{"line":14,"column":0},"end":{"line":14,"column":54}},"3":{"start":{"line":15,"column":0},"end":{"line":15,"column":54}},"4":{"start":{"line":16,"column":0},"end":{"line":16,"column":64}},"5":{"start":{"line":18,"column":0},"end":{"line":18,"column":73}},"6":{"start":{"line":20,"column":0},"end":{"line":20,"column":73}},"7":{"start":{"line":25,"column":7},"end":{"line":199,"column":null}},"8":{"start":{"line":28,"column":25},"end":{"line":28,"column":50}},"9":{"start":{"line":30,"column":25},"end":{"line":30,"column":50}},"10":{"start":{"line":31,"column":25},"end":{"line":31,"column":39}},"11":{"start":{"line":35,"column":21},"end":{"line":53,"column":10}},"12":{"start":{"line":55,"column":25},"end":{"line":57,"column":10}},"13":{"start":{"line":59,"column":8},"end":{"line":63,"column":9}},"14":{"start":{"line":60,"column":12},"end":{"line":62,"column":14}},"15":{"start":{"line":65,"column":24},"end":{"line":65,"column":53}},"16":{"start":{"line":67,"column":8},"end":{"line":72,"column":9}},"17":{"start":{"line":68,"column":12},"end":{"line":71,"column":14}},"18":{"start":{"line":74,"column":8},"end":{"line":74,"column":23}},"19":{"start":{"line":78,"column":8},"end":{"line":78,"column":40}},"20":{"start":{"line":85,"column":21},"end":{"line":85,"column":56}},"21":{"start":{"line":86,"column":8},"end":{"line":86,"column":65}},"22":{"start":{"line":86,"column":19},"end":{"line":86,"column":65}},"23":{"start":{"line":87,"column":8},"end":{"line":87,"column":20}},"24":{"start":{"line":94,"column":8},"end":{"line":94,"column":51}},"25":{"start":{"line":98,"column":24},"end":{"line":98,"column":46}},"26":{"start":{"line":100,"column":12},"end":{"line":102,"column":40}},"27":{"start":{"line":104,"column":25},"end":{"line":110,"column":23}},"28":{"start":{"line":112,"column":43},"end":{"line":115,"column":10}},"29":{"start":{"line":117,"column":8},"end":{"line":117,"column":50}},"30":{"start":{"line":121,"column":8},"end":{"line":121,"column":31}},"31":{"start":{"line":122,"column":8},"end":{"line":122,"column":40}},"32":{"start":{"line":129,"column":8},"end":{"line":129,"column":35}},"33":{"start":{"line":131,"column":28},"end":{"line":131,"column":54}},"34":{"start":{"line":132,"column":22},"end":{"line":132,"column":63}},"35":{"start":{"line":133,"column":8},"end":{"line":139,"column":9}},"36":{"start":{"line":134,"column":31},"end":{"line":134,"column":64}},"37":{"start":{"line":134,"column":56},"end":{"line":134,"column":62}},"38":{"start":{"line":135,"column":28},"end":{"line":135,"column":73}},"39":{"start":{"line":135,"column":54},"end":{"line":135,"column":72}},"40":{"start":{"line":136,"column":12},"end":{"line":138,"column":14}},"41":{"start":{"line":141,"column":8},"end":{"line":141,"column":60}},"42":{"start":{"line":151,"column":25},"end":{"line":153,"column":10}},"43":{"start":{"line":155,"column":8},"end":{"line":157,"column":9}},"44":{"start":{"line":156,"column":12},"end":{"line":156,"column":28}},"45":{"start":{"line":159,"column":28},"end":{"line":175,"column":10}},"46":{"start":{"line":177,"column":26},"end":{"line":177,"column":62}},"47":{"start":{"line":179,"column":8},"end":{"line":184,"column":9}},"48":{"start":{"line":180,"column":12},"end":{"line":183,"column":14}},"49":{"start":{"line":186,"column":8},"end":{"line":186,"column":25}},"50":{"start":{"line":194,"column":22},"end":{"line":195,"column":null}},"51":{"start":{"line":197,"column":8},"end":{"line":197,"column":31}},"52":{"start":{"line":25,"column":13},"end":{"line":25,"column":32}},"53":{"start":{"line":25,"column":13},"end":{"line":199,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":26,"column":4},"end":{"line":26,"column":null}},"loc":{"start":{"line":31,"column":52},"end":{"line":32,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":34,"column":4},"end":{"line":34,"column":9}},"loc":{"start":{"line":34,"column":43},"end":{"line":75,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":77,"column":4},"end":{"line":77,"column":9}},"loc":{"start":{"line":77,"column":39},"end":{"line":79,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":81,"column":4},"end":{"line":81,"column":9}},"loc":{"start":{"line":83,"column":38},"end":{"line":88,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":90,"column":4},"end":{"line":90,"column":9}},"loc":{"start":{"line":92,"column":38},"end":{"line":95,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":97,"column":4},"end":{"line":97,"column":9}},"loc":{"start":{"line":97,"column":53},"end":{"line":118,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":120,"column":4},"end":{"line":120,"column":9}},"loc":{"start":{"line":120,"column":27},"end":{"line":123,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":125,"column":4},"end":{"line":125,"column":9}},"loc":{"start":{"line":127,"column":29},"end":{"line":142,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":134,"column":49},"end":{"line":134,"column":50}},"loc":{"start":{"line":134,"column":56},"end":{"line":134,"column":62}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":135,"column":47},"end":{"line":135,"column":48}},"loc":{"start":{"line":135,"column":54},"end":{"line":135,"column":72}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":144,"column":4},"end":{"line":144,"column":9}},"loc":{"start":{"line":150,"column":5},"end":{"line":187,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":189,"column":12},"end":{"line":189,"column":25}},"loc":{"start":{"line":192,"column":38},"end":{"line":198,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":40,"column":16},"end":{"line":44,"column":null}},"type":"binary-expr","locations":[{"start":{"line":40,"column":16},"end":{"line":40,"column":32}},{"start":{"line":41,"column":16},"end":{"line":44,"column":null}}]},"1":{"loc":{"start":{"line":48,"column":20},"end":{"line":48,"column":59}},"type":"binary-expr","locations":[{"start":{"line":48,"column":20},"end":{"line":48,"column":34}},{"start":{"line":48,"column":38},"end":{"line":48,"column":59}}]},"2":{"loc":{"start":{"line":49,"column":24},"end":{"line":49,"column":50}},"type":"binary-expr","locations":[{"start":{"line":49,"column":24},"end":{"line":49,"column":42}},{"start":{"line":49,"column":46},"end":{"line":49,"column":50}}]},"3":{"loc":{"start":{"line":50,"column":20},"end":{"line":50,"column":42}},"type":"binary-expr","locations":[{"start":{"line":50,"column":20},"end":{"line":50,"column":34}},{"start":{"line":50,"column":38},"end":{"line":50,"column":42}}]},"4":{"loc":{"start":{"line":51,"column":23},"end":{"line":51,"column":48}},"type":"binary-expr","locations":[{"start":{"line":51,"column":23},"end":{"line":51,"column":40}},{"start":{"line":51,"column":44},"end":{"line":51,"column":48}}]},"5":{"loc":{"start":{"line":52,"column":19},"end":{"line":52,"column":38}},"type":"binary-expr","locations":[{"start":{"line":52,"column":19},"end":{"line":52,"column":32}},{"start":{"line":52,"column":36},"end":{"line":52,"column":38}}]},"6":{"loc":{"start":{"line":59,"column":8},"end":{"line":63,"column":9}},"type":"if","locations":[{"start":{"line":59,"column":8},"end":{"line":63,"column":9}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":67,"column":8},"end":{"line":72,"column":9}},"type":"if","locations":[{"start":{"line":67,"column":8},"end":{"line":72,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":86,"column":8},"end":{"line":86,"column":65}},"type":"if","locations":[{"start":{"line":86,"column":8},"end":{"line":86,"column":65}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":100,"column":12},"end":{"line":102,"column":40}},"type":"binary-expr","locations":[{"start":{"line":100,"column":12},"end":{"line":100,"column":41}},{"start":{"line":101,"column":12},"end":{"line":101,"column":42}},{"start":{"line":102,"column":12},"end":{"line":102,"column":40}}]},"10":{"loc":{"start":{"line":104,"column":25},"end":{"line":110,"column":23}},"type":"cond-expr","locations":[{"start":{"line":105,"column":14},"end":{"line":108,"column":null}},{"start":{"line":110,"column":14},"end":{"line":110,"column":23}}]},"11":{"loc":{"start":{"line":106,"column":18},"end":{"line":106,"column":52}},"type":"binary-expr","locations":[{"start":{"line":106,"column":18},"end":{"line":106,"column":32}},{"start":{"line":106,"column":36},"end":{"line":106,"column":52}}]},"12":{"loc":{"start":{"line":107,"column":18},"end":{"line":107,"column":54}},"type":"binary-expr","locations":[{"start":{"line":107,"column":18},"end":{"line":107,"column":33}},{"start":{"line":107,"column":37},"end":{"line":107,"column":54}}]},"13":{"loc":{"start":{"line":108,"column":18},"end":{"line":108,"column":56}},"type":"binary-expr","locations":[{"start":{"line":108,"column":18},"end":{"line":108,"column":34}},{"start":{"line":108,"column":38},"end":{"line":108,"column":56}}]},"14":{"loc":{"start":{"line":114,"column":16},"end":{"line":114,"column":58}},"type":"cond-expr","locations":[{"start":{"line":114,"column":41},"end":{"line":114,"column":53}},{"start":{"line":114,"column":56},"end":{"line":114,"column":58}}]},"15":{"loc":{"start":{"line":133,"column":8},"end":{"line":139,"column":9}},"type":"if","locations":[{"start":{"line":133,"column":8},"end":{"line":139,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":155,"column":8},"end":{"line":157,"column":9}},"type":"if","locations":[{"start":{"line":155,"column":8},"end":{"line":157,"column":9}},{"start":{},"end":{}}]},"17":{"loc":{"start":{"line":179,"column":8},"end":{"line":184,"column":9}},"type":"if","locations":[{"start":{"line":179,"column":8},"end":{"line":184,"column":9}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":194,"column":44},"end":{"line":194,"column":67}},"type":"binary-expr","locations":[{"start":{"line":194,"column":44},"end":{"line":194,"column":54}},{"start":{"line":194,"column":58},"end":{"line":194,"column":67}}]},"19":{"loc":{"start":{"line":31,"column":39},"end":{"line":31,"column":null}},"type":"cond-expr","locations":[{"start":{"line":31,"column":52},"end":{"line":31,"column":null}},{"start":{"line":31,"column":52},"end":{"line":31,"column":null}}]},"20":{"loc":{"start":{"line":31,"column":39},"end":{"line":31,"column":52}},"type":"binary-expr","locations":[{"start":{"line":31,"column":39},"end":{"line":31,"column":null}},{"start":{"line":31,"column":39},"end":{"line":31,"column":52}}]}},"s":{"0":57,"1":57,"2":57,"3":57,"4":57,"5":57,"6":57,"7":57,"8":57,"9":57,"10":57,"11":671,"12":671,"13":671,"14":1,"15":670,"16":670,"17":670,"18":670,"19":2,"20":27,"21":27,"22":6,"23":21,"24":2,"25":3,"26":2,"27":2,"28":2,"29":2,"30":2,"31":1,"32":3,"33":3,"34":3,"35":3,"36":0,"37":0,"38":0,"39":0,"40":0,"41":3,"42":3,"43":3,"44":1,"45":2,"46":2,"47":2,"48":2,"49":2,"50":673,"51":673,"52":57,"53":57},"f":{"0":57,"1":671,"2":2,"3":27,"4":2,"5":3,"6":2,"7":3,"8":0,"9":0,"10":3,"11":673},"b":{"0":[671,670],"1":[671,670],"2":[671,671],"3":[671,671],"4":[671,671],"5":[671,665],"6":[1,670],"7":[670,0],"8":[6,21],"9":[2,1,1],"10":[1,1],"11":[1,1],"12":[1,0],"13":[1,1],"14":[1,1],"15":[0,3],"16":[1,2],"17":[2,0],"18":[673,673],"19":[57,0],"20":[57,57]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/infrastructure/prisma-repositories/role.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/infrastructure/prisma-repositories/role.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":44}},"1":{"start":{"line":3,"column":0},"end":{"line":3,"column":60}},"2":{"start":{"line":5,"column":0},"end":{"line":5,"column":52}},"3":{"start":{"line":8,"column":7},"end":{"line":25,"column":null}},"4":{"start":{"line":9,"column":33},"end":{"line":9,"column":41}},"5":{"start":{"line":12,"column":22},"end":{"line":14,"column":10}},"6":{"start":{"line":15,"column":8},"end":{"line":15,"column":46}},"7":{"start":{"line":19,"column":8},"end":{"line":19,"column":37}},"8":{"start":{"line":19,"column":27},"end":{"line":19,"column":37}},"9":{"start":{"line":20,"column":22},"end":{"line":22,"column":10}},"10":{"start":{"line":23,"column":8},"end":{"line":23,"column":46}},"11":{"start":{"line":8,"column":13},"end":{"line":8,"column":27}},"12":{"start":{"line":8,"column":13},"end":{"line":25,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":9,"column":4},"end":{"line":9,"column":33}},"loc":{"start":{"line":9,"column":54},"end":{"line":9,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":11,"column":4},"end":{"line":11,"column":9}},"loc":{"start":{"line":11,"column":16},"end":{"line":16,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":18,"column":43},"end":{"line":24,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":19,"column":8},"end":{"line":19,"column":37}},"type":"if","locations":[{"start":{"line":19,"column":8},"end":{"line":19,"column":37}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":9,"column":41},"end":{"line":9,"column":57}},"type":"cond-expr","locations":[{"start":{"line":9,"column":54},"end":{"line":9,"column":57}},{"start":{"line":9,"column":54},"end":{"line":9,"column":57}}]},"2":{"loc":{"start":{"line":9,"column":41},"end":{"line":9,"column":54}},"type":"binary-expr","locations":[{"start":{"line":9,"column":41},"end":{"line":9,"column":57}},{"start":{"line":9,"column":41},"end":{"line":9,"column":54}}]}},"s":{"0":57,"1":57,"2":57,"3":57,"4":57,"5":6,"6":6,"7":9,"8":3,"9":6,"10":6,"11":57,"12":57},"f":{"0":57,"1":6,"2":9},"b":{"0":[3,6],"1":[57,0],"2":[57,57]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/infrastructure/prisma-repositories/user.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/infrastructure/prisma-repositories/user.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":9,"column":0},"end":{"line":9,"column":44}},"2":{"start":{"line":10,"column":0},"end":{"line":10,"column":60}},"3":{"start":{"line":13,"column":0},"end":{"line":13,"column":67}},"4":{"start":{"line":16,"column":7},"end":{"line":199,"column":null}},"5":{"start":{"line":17,"column":33},"end":{"line":17,"column":41}},"6":{"start":{"line":20,"column":24},"end":{"line":23,"column":10}},"7":{"start":{"line":25,"column":8},"end":{"line":25,"column":44}},"8":{"start":{"line":32,"column":21},"end":{"line":35,"column":10}},"9":{"start":{"line":37,"column":8},"end":{"line":37,"column":55}},"10":{"start":{"line":44,"column":21},"end":{"line":47,"column":10}},"11":{"start":{"line":49,"column":8},"end":{"line":49,"column":55}},"12":{"start":{"line":53,"column":22},"end":{"line":53,"column":44}},"13":{"start":{"line":54,"column":24},"end":{"line":54,"column":46}},"14":{"start":{"line":56,"column":22},"end":{"line":60,"column":10}},"15":{"start":{"line":62,"column":8},"end":{"line":62,"column":73}},"16":{"start":{"line":62,"column":32},"end":{"line":62,"column":71}},"17":{"start":{"line":69,"column":24},"end":{"line":78,"column":10}},"18":{"start":{"line":80,"column":8},"end":{"line":80,"column":44}},"19":{"start":{"line":84,"column":8},"end":{"line":84,"column":57}},"20":{"start":{"line":91,"column":8},"end":{"line":108,"column":11}},"21":{"start":{"line":92,"column":59},"end":{"line":94,"column":28}},"22":{"start":{"line":96,"column":12},"end":{"line":96,"column":65}},"23":{"start":{"line":98,"column":29},"end":{"line":98,"column":78}},"24":{"start":{"line":99,"column":34},"end":{"line":99,"column":74}},"25":{"start":{"line":99,"column":62},"end":{"line":99,"column":72}},"26":{"start":{"line":100,"column":29},"end":{"line":100,"column":77}},"27":{"start":{"line":100,"column":52},"end":{"line":100,"column":76}},"28":{"start":{"line":102,"column":12},"end":{"line":107,"column":13}},"29":{"start":{"line":103,"column":16},"end":{"line":106,"column":19}},"30":{"start":{"line":104,"column":54},"end":{"line":104,"column":74}},"31":{"start":{"line":110,"column":26},"end":{"line":110,"column":74}},"32":{"start":{"line":111,"column":8},"end":{"line":113,"column":9}},"33":{"start":{"line":112,"column":12},"end":{"line":112,"column":65}},"34":{"start":{"line":114,"column":8},"end":{"line":114,"column":25}},"35":{"start":{"line":130,"column":12},"end":{"line":130,"column":17}},"36":{"start":{"line":132,"column":8},"end":{"line":180,"column":10}},"37":{"start":{"line":186,"column":22},"end":{"line":186,"column":54}},"38":{"start":{"line":187,"column":26},"end":{"line":187,"column":66}},"39":{"start":{"line":188,"column":8},"end":{"line":188,"column":74}},"40":{"start":{"line":192,"column":8},"end":{"line":192,"column":41}},"41":{"start":{"line":192,"column":24},"end":{"line":192,"column":41}},"42":{"start":{"line":193,"column":8},"end":{"line":197,"column":10}},"43":{"start":{"line":16,"column":13},"end":{"line":16,"column":27}},"44":{"start":{"line":16,"column":13},"end":{"line":199,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":17,"column":4},"end":{"line":17,"column":33}},"loc":{"start":{"line":17,"column":54},"end":{"line":17,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":19,"column":4},"end":{"line":19,"column":9}},"loc":{"start":{"line":19,"column":33},"end":{"line":26,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":28,"column":4},"end":{"line":28,"column":9}},"loc":{"start":{"line":30,"column":38},"end":{"line":38,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":40,"column":4},"end":{"line":40,"column":9}},"loc":{"start":{"line":42,"column":38},"end":{"line":50,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":52,"column":4},"end":{"line":52,"column":9}},"loc":{"start":{"line":52,"column":39},"end":{"line":63,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":62,"column":25},"end":{"line":62,"column":26}},"loc":{"start":{"line":62,"column":32},"end":{"line":62,"column":71}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":65,"column":4},"end":{"line":65,"column":9}},"loc":{"start":{"line":67,"column":32},"end":{"line":81,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":83,"column":4},"end":{"line":83,"column":9}},"loc":{"start":{"line":83,"column":31},"end":{"line":85,"column":5}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":87,"column":4},"end":{"line":87,"column":9}},"loc":{"start":{"line":89,"column":29},"end":{"line":115,"column":5}}},"9":{"name":"(anonymous_11)","decl":{"start":{"line":91,"column":39},"end":{"line":91,"column":44}},"loc":{"start":{"line":91,"column":52},"end":{"line":108,"column":9}}},"10":{"name":"(anonymous_12)","decl":{"start":{"line":99,"column":55},"end":{"line":99,"column":56}},"loc":{"start":{"line":99,"column":62},"end":{"line":99,"column":72}}},"11":{"name":"(anonymous_13)","decl":{"start":{"line":100,"column":42},"end":{"line":100,"column":43}},"loc":{"start":{"line":100,"column":52},"end":{"line":100,"column":76}}},"12":{"name":"(anonymous_14)","decl":{"start":{"line":104,"column":39},"end":{"line":104,"column":40}},"loc":{"start":{"line":104,"column":54},"end":{"line":104,"column":74}}},"13":{"name":"(anonymous_15)","decl":{"start":{"line":117,"column":12},"end":{"line":117,"column":22}},"loc":{"start":{"line":117,"column":45},"end":{"line":181,"column":5}}},"14":{"name":"(anonymous_16)","decl":{"start":{"line":183,"column":12},"end":{"line":183,"column":22}},"loc":{"start":{"line":184,"column":30},"end":{"line":189,"column":5}}},"15":{"name":"(anonymous_17)","decl":{"start":{"line":191,"column":12},"end":{"line":191,"column":28}},"loc":{"start":{"line":191,"column":48},"end":{"line":198,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":37,"column":15},"end":{"line":37,"column":54}},"type":"cond-expr","locations":[{"start":{"line":37,"column":22},"end":{"line":37,"column":47}},{"start":{"line":37,"column":50},"end":{"line":37,"column":54}}]},"1":{"loc":{"start":{"line":49,"column":15},"end":{"line":49,"column":54}},"type":"cond-expr","locations":[{"start":{"line":49,"column":22},"end":{"line":49,"column":47}},{"start":{"line":49,"column":50},"end":{"line":49,"column":54}}]},"2":{"loc":{"start":{"line":73,"column":28},"end":{"line":73,"column":52}},"type":"binary-expr","locations":[{"start":{"line":73,"column":28},"end":{"line":73,"column":44}},{"start":{"line":73,"column":48},"end":{"line":73,"column":52}}]},"3":{"loc":{"start":{"line":74,"column":24},"end":{"line":74,"column":44}},"type":"binary-expr","locations":[{"start":{"line":74,"column":24},"end":{"line":74,"column":36}},{"start":{"line":74,"column":40},"end":{"line":74,"column":44}}]},"4":{"loc":{"start":{"line":75,"column":27},"end":{"line":75,"column":50}},"type":"binary-expr","locations":[{"start":{"line":75,"column":27},"end":{"line":75,"column":42}},{"start":{"line":75,"column":46},"end":{"line":75,"column":50}}]},"5":{"loc":{"start":{"line":92,"column":59},"end":{"line":94,"column":28}},"type":"cond-expr","locations":[{"start":{"line":93,"column":18},"end":{"line":93,"column":62}},{"start":{"line":94,"column":18},"end":{"line":94,"column":28}}]},"6":{"loc":{"start":{"line":102,"column":12},"end":{"line":107,"column":13}},"type":"if","locations":[{"start":{"line":102,"column":12},"end":{"line":107,"column":13}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":111,"column":8},"end":{"line":113,"column":9}},"type":"if","locations":[{"start":{"line":111,"column":8},"end":{"line":113,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":133,"column":16},"end":{"line":135,"column":20}},"type":"cond-expr","locations":[{"start":{"line":134,"column":18},"end":{"line":134,"column":77}},{"start":{"line":135,"column":18},"end":{"line":135,"column":20}}]},"9":{"loc":{"start":{"line":136,"column":16},"end":{"line":138,"column":20}},"type":"cond-expr","locations":[{"start":{"line":137,"column":18},"end":{"line":137,"column":79}},{"start":{"line":138,"column":18},"end":{"line":138,"column":20}}]},"10":{"loc":{"start":{"line":139,"column":16},"end":{"line":141,"column":20}},"type":"cond-expr","locations":[{"start":{"line":140,"column":18},"end":{"line":140,"column":75}},{"start":{"line":141,"column":18},"end":{"line":141,"column":20}}]},"11":{"loc":{"start":{"line":142,"column":16},"end":{"line":144,"column":20}},"type":"cond-expr","locations":[{"start":{"line":143,"column":18},"end":{"line":143,"column":75}},{"start":{"line":144,"column":18},"end":{"line":144,"column":20}}]},"12":{"loc":{"start":{"line":145,"column":16},"end":{"line":169,"column":20}},"type":"cond-expr","locations":[{"start":{"line":146,"column":18},"end":{"line":168,"column":null}},{"start":{"line":169,"column":18},"end":{"line":169,"column":20}}]},"13":{"loc":{"start":{"line":170,"column":16},"end":{"line":172,"column":20}},"type":"cond-expr","locations":[{"start":{"line":171,"column":18},"end":{"line":171,"column":69}},{"start":{"line":172,"column":18},"end":{"line":172,"column":20}}]},"14":{"loc":{"start":{"line":173,"column":16},"end":{"line":173,"column":40}},"type":"cond-expr","locations":[{"start":{"line":173,"column":25},"end":{"line":173,"column":35}},{"start":{"line":173,"column":38},"end":{"line":173,"column":40}}]},"15":{"loc":{"start":{"line":174,"column":16},"end":{"line":174,"column":40}},"type":"cond-expr","locations":[{"start":{"line":174,"column":25},"end":{"line":174,"column":35}},{"start":{"line":174,"column":38},"end":{"line":174,"column":40}}]},"16":{"loc":{"start":{"line":175,"column":16},"end":{"line":175,"column":62}},"type":"cond-expr","locations":[{"start":{"line":175,"column":43},"end":{"line":175,"column":57}},{"start":{"line":175,"column":60},"end":{"line":175,"column":62}}]},"17":{"loc":{"start":{"line":176,"column":16},"end":{"line":176,"column":46}},"type":"cond-expr","locations":[{"start":{"line":176,"column":28},"end":{"line":176,"column":41}},{"start":{"line":176,"column":44},"end":{"line":176,"column":46}}]},"18":{"loc":{"start":{"line":177,"column":16},"end":{"line":179,"column":20}},"type":"cond-expr","locations":[{"start":{"line":178,"column":18},"end":{"line":178,"column":70}},{"start":{"line":179,"column":18},"end":{"line":179,"column":20}}]},"19":{"loc":{"start":{"line":186,"column":22},"end":{"line":186,"column":54}},"type":"binary-expr","locations":[{"start":{"line":186,"column":22},"end":{"line":186,"column":34}},{"start":{"line":186,"column":38},"end":{"line":186,"column":54}}]},"20":{"loc":{"start":{"line":187,"column":26},"end":{"line":187,"column":66}},"type":"binary-expr","locations":[{"start":{"line":187,"column":26},"end":{"line":187,"column":45}},{"start":{"line":187,"column":49},"end":{"line":187,"column":66}}]},"21":{"loc":{"start":{"line":192,"column":8},"end":{"line":192,"column":41}},"type":"if","locations":[{"start":{"line":192,"column":8},"end":{"line":192,"column":41}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":17,"column":41},"end":{"line":17,"column":57}},"type":"cond-expr","locations":[{"start":{"line":17,"column":54},"end":{"line":17,"column":57}},{"start":{"line":17,"column":54},"end":{"line":17,"column":57}}]},"23":{"loc":{"start":{"line":17,"column":41},"end":{"line":17,"column":54}},"type":"binary-expr","locations":[{"start":{"line":17,"column":41},"end":{"line":17,"column":57}},{"start":{"line":17,"column":41},"end":{"line":17,"column":54}}]}},"s":{"0":57,"1":57,"2":57,"3":57,"4":57,"5":57,"6":742,"7":742,"8":56,"9":56,"10":678,"11":678,"12":8,"13":8,"14":8,"15":8,"16":13,"17":4,"18":4,"19":2,"20":8,"21":8,"22":8,"23":8,"24":8,"25":6,"26":8,"27":11,"28":8,"29":3,"30":5,"31":8,"32":8,"33":0,"34":8,"35":8,"36":8,"37":8,"38":8,"39":8,"40":1488,"41":49,"42":1439,"43":57,"44":57},"f":{"0":57,"1":742,"2":56,"3":678,"4":8,"5":13,"6":4,"7":2,"8":8,"9":8,"10":6,"11":11,"12":5,"13":8,"14":8,"15":1488},"b":{"0":[48,8],"1":[4,674],"2":[4,4],"3":[4,4],"4":[4,4],"5":[6,2],"6":[3,5],"7":[0,8],"8":[0,8],"9":[0,8],"10":[1,7],"11":[0,8],"12":[3,5],"13":[0,8],"14":[2,6],"15":[0,8],"16":[0,8],"17":[0,8],"18":[0,8],"19":[8,7],"20":[8,7],"21":[49,1439],"22":[57,0],"23":[57,57]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/cluster.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/cluster.service.ts","statementMap":{"0":{"start":{"line":6,"column":0},"end":{"line":6,"column":null}},"1":{"start":{"line":12,"column":0},"end":{"line":12,"column":60}},"2":{"start":{"line":13,"column":0},"end":{"line":13,"column":null}},"3":{"start":{"line":17,"column":0},"end":{"line":17,"column":57}},"4":{"start":{"line":20,"column":7},"end":{"line":99,"column":null}},"5":{"start":{"line":23,"column":25},"end":{"line":23,"column":35}},"6":{"start":{"line":24,"column":25},"end":{"line":24,"column":38}},"7":{"start":{"line":28,"column":8},"end":{"line":28,"column":72}},"8":{"start":{"line":29,"column":8},"end":{"line":29,"column":61}},"9":{"start":{"line":31,"column":24},"end":{"line":37,"column":10}},"10":{"start":{"line":39,"column":8},"end":{"line":39,"column":45}},"11":{"start":{"line":43,"column":8},"end":{"line":43,"column":43}},"12":{"start":{"line":47,"column":24},"end":{"line":47,"column":56}},"13":{"start":{"line":48,"column":8},"end":{"line":48,"column":71}},"14":{"start":{"line":48,"column":22},"end":{"line":48,"column":71}},"15":{"start":{"line":49,"column":8},"end":{"line":49,"column":23}},"16":{"start":{"line":56,"column":24},"end":{"line":56,"column":46}},"17":{"start":{"line":58,"column":8},"end":{"line":60,"column":9}},"18":{"start":{"line":59,"column":12},"end":{"line":59,"column":63}},"19":{"start":{"line":62,"column":8},"end":{"line":66,"column":9}},"20":{"start":{"line":63,"column":26},"end":{"line":63,"column":64}},"21":{"start":{"line":64,"column":26},"end":{"line":64,"column":64}},"22":{"start":{"line":65,"column":12},"end":{"line":65,"column":50}},"23":{"start":{"line":68,"column":46},"end":{"line":82,"column":10}},"24":{"start":{"line":84,"column":8},"end":{"line":84,"column":53}},"25":{"start":{"line":88,"column":8},"end":{"line":88,"column":31}},"26":{"start":{"line":89,"column":8},"end":{"line":89,"column":43}},"27":{"start":{"line":93,"column":8},"end":{"line":97,"column":9}},"28":{"start":{"line":94,"column":12},"end":{"line":96,"column":14}},"29":{"start":{"line":20,"column":13},"end":{"line":20,"column":27}},"30":{"start":{"line":20,"column":13},"end":{"line":99,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"loc":{"start":{"line":24,"column":55},"end":{"line":25,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":27,"column":4},"end":{"line":27,"column":9}},"loc":{"start":{"line":27,"column":46},"end":{"line":40,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":42,"column":4},"end":{"line":42,"column":9}},"loc":{"start":{"line":42,"column":42},"end":{"line":44,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":46,"column":4},"end":{"line":46,"column":9}},"loc":{"start":{"line":46,"column":28},"end":{"line":50,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":52,"column":4},"end":{"line":52,"column":9}},"loc":{"start":{"line":54,"column":35},"end":{"line":85,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":87,"column":4},"end":{"line":87,"column":9}},"loc":{"start":{"line":87,"column":27},"end":{"line":90,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":92,"column":12},"end":{"line":92,"column":17}},"loc":{"start":{"line":92,"column":59},"end":{"line":98,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":48,"column":8},"end":{"line":48,"column":71}},"type":"if","locations":[{"start":{"line":48,"column":8},"end":{"line":48,"column":71}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":58,"column":8},"end":{"line":60,"column":9}},"type":"if","locations":[{"start":{"line":58,"column":8},"end":{"line":60,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":58,"column":12},"end":{"line":58,"column":77}},"type":"binary-expr","locations":[{"start":{"line":58,"column":12},"end":{"line":58,"column":30}},{"start":{"line":58,"column":34},"end":{"line":58,"column":77}}]},"3":{"loc":{"start":{"line":62,"column":8},"end":{"line":66,"column":9}},"type":"if","locations":[{"start":{"line":62,"column":8},"end":{"line":66,"column":9}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":62,"column":12},"end":{"line":62,"column":76}},"type":"binary-expr","locations":[{"start":{"line":62,"column":12},"end":{"line":62,"column":42}},{"start":{"line":62,"column":46},"end":{"line":62,"column":76}}]},"5":{"loc":{"start":{"line":63,"column":26},"end":{"line":63,"column":64}},"type":"binary-expr","locations":[{"start":{"line":63,"column":26},"end":{"line":63,"column":42}},{"start":{"line":63,"column":46},"end":{"line":63,"column":64}}]},"6":{"loc":{"start":{"line":64,"column":26},"end":{"line":64,"column":64}},"type":"binary-expr","locations":[{"start":{"line":64,"column":26},"end":{"line":64,"column":42}},{"start":{"line":64,"column":46},"end":{"line":64,"column":64}}]},"7":{"loc":{"start":{"line":69,"column":16},"end":{"line":71,"column":20}},"type":"cond-expr","locations":[{"start":{"line":70,"column":18},"end":{"line":70,"column":54}},{"start":{"line":71,"column":18},"end":{"line":71,"column":20}}]},"8":{"loc":{"start":{"line":72,"column":16},"end":{"line":74,"column":20}},"type":"cond-expr","locations":[{"start":{"line":73,"column":18},"end":{"line":73,"column":50}},{"start":{"line":74,"column":18},"end":{"line":74,"column":20}}]},"9":{"loc":{"start":{"line":75,"column":16},"end":{"line":77,"column":20}},"type":"cond-expr","locations":[{"start":{"line":76,"column":18},"end":{"line":76,"column":50}},{"start":{"line":77,"column":18},"end":{"line":77,"column":20}}]},"10":{"loc":{"start":{"line":78,"column":16},"end":{"line":78,"column":71}},"type":"cond-expr","locations":[{"start":{"line":78,"column":44},"end":{"line":78,"column":66}},{"start":{"line":78,"column":69},"end":{"line":78,"column":71}}]},"11":{"loc":{"start":{"line":79,"column":16},"end":{"line":81,"column":20}},"type":"cond-expr","locations":[{"start":{"line":80,"column":18},"end":{"line":80,"column":52}},{"start":{"line":81,"column":18},"end":{"line":81,"column":20}}]},"12":{"loc":{"start":{"line":93,"column":8},"end":{"line":97,"column":9}},"type":"if","locations":[{"start":{"line":93,"column":8},"end":{"line":97,"column":9}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":23,"column":35},"end":{"line":23,"column":null}},"type":"cond-expr","locations":[{"start":{"line":23,"column":56},"end":{"line":23,"column":null}},{"start":{"line":23,"column":56},"end":{"line":23,"column":null}}]},"14":{"loc":{"start":{"line":23,"column":35},"end":{"line":23,"column":56}},"type":"binary-expr","locations":[{"start":{"line":23,"column":35},"end":{"line":23,"column":null}},{"start":{"line":23,"column":35},"end":{"line":23,"column":56}}]},"15":{"loc":{"start":{"line":23,"column":56},"end":{"line":24,"column":null}},"type":"cond-expr","locations":[{"start":{"line":24,"column":55},"end":{"line":24,"column":null}},{"start":{"line":24,"column":55},"end":{"line":24,"column":null}}]},"16":{"loc":{"start":{"line":23,"column":56},"end":{"line":24,"column":55}},"type":"binary-expr","locations":[{"start":{"line":23,"column":56},"end":{"line":24,"column":null}},{"start":{"line":24,"column":38},"end":{"line":24,"column":55}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":44,"7":28,"8":27,"9":26,"10":26,"11":8,"12":7,"13":7,"14":2,"15":5,"16":3,"17":3,"18":1,"19":2,"20":1,"21":1,"22":1,"23":1,"24":1,"25":2,"26":1,"27":29,"28":2,"29":44,"30":44},"f":{"0":44,"1":28,"2":8,"3":7,"4":3,"5":2,"6":29},"b":{"0":[2,5],"1":[1,2],"2":[3,1],"3":[1,1],"4":[2,1],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[1,0],"12":[2,27],"13":[0,44],"14":[44,0],"15":[44,0],"16":[44,44]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/competence.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/competence.service.ts","statementMap":{"0":{"start":{"line":6,"column":0},"end":{"line":6,"column":null}},"1":{"start":{"line":12,"column":0},"end":{"line":12,"column":98}},"2":{"start":{"line":13,"column":0},"end":{"line":13,"column":66}},"3":{"start":{"line":14,"column":0},"end":{"line":14,"column":null}},"4":{"start":{"line":18,"column":0},"end":{"line":18,"column":null}},"5":{"start":{"line":22,"column":0},"end":{"line":22,"column":null}},"6":{"start":{"line":26,"column":0},"end":{"line":26,"column":null}},"7":{"start":{"line":32,"column":7},"end":{"line":172,"column":null}},"8":{"start":{"line":35,"column":25},"end":{"line":35,"column":38}},"9":{"start":{"line":37,"column":25},"end":{"line":37,"column":44}},"10":{"start":{"line":39,"column":25},"end":{"line":39,"column":54}},"11":{"start":{"line":41,"column":25},"end":{"line":41,"column":62}},"12":{"start":{"line":42,"column":25},"end":{"line":42,"column":36}},"13":{"start":{"line":46,"column":27},"end":{"line":50,"column":10}},"14":{"start":{"line":51,"column":8},"end":{"line":51,"column":51}},"15":{"start":{"line":55,"column":8},"end":{"line":55,"column":46}},"16":{"start":{"line":59,"column":27},"end":{"line":59,"column":62}},"17":{"start":{"line":60,"column":8},"end":{"line":60,"column":77}},"18":{"start":{"line":60,"column":25},"end":{"line":60,"column":77}},"19":{"start":{"line":61,"column":8},"end":{"line":61,"column":26}},"20":{"start":{"line":68,"column":8},"end":{"line":68,"column":31}},"21":{"start":{"line":69,"column":49},"end":{"line":75,"column":10}},"22":{"start":{"line":76,"column":8},"end":{"line":76,"column":56}},"23":{"start":{"line":80,"column":8},"end":{"line":80,"column":31}},"24":{"start":{"line":81,"column":41},"end":{"line":81,"column":77}},"25":{"start":{"line":82,"column":33},"end":{"line":82,"column":61}},"26":{"start":{"line":83,"column":8},"end":{"line":96,"column":9}},"27":{"start":{"line":87,"column":12},"end":{"line":95,"column":14}},"28":{"start":{"line":97,"column":8},"end":{"line":97,"column":74}},"29":{"start":{"line":98,"column":8},"end":{"line":100,"column":10}},"30":{"start":{"line":101,"column":8},"end":{"line":101,"column":46}},"31":{"start":{"line":108,"column":8},"end":{"line":108,"column":41}},"32":{"start":{"line":109,"column":8},"end":{"line":109,"column":49}},"33":{"start":{"line":111,"column":8},"end":{"line":111,"column":78}},"34":{"start":{"line":112,"column":8},"end":{"line":112,"column":48}},"35":{"start":{"line":119,"column":8},"end":{"line":119,"column":41}},"36":{"start":{"line":120,"column":8},"end":{"line":120,"column":80}},"37":{"start":{"line":124,"column":8},"end":{"line":124,"column":41}},"38":{"start":{"line":126,"column":12},"end":{"line":127,"column":null}},"39":{"start":{"line":129,"column":8},"end":{"line":129,"column":50}},"40":{"start":{"line":129,"column":36},"end":{"line":129,"column":48}},"41":{"start":{"line":136,"column":8},"end":{"line":136,"column":41}},"42":{"start":{"line":137,"column":8},"end":{"line":137,"column":68}},"43":{"start":{"line":139,"column":8},"end":{"line":142,"column":10}},"44":{"start":{"line":143,"column":8},"end":{"line":143,"column":56}},"45":{"start":{"line":150,"column":8},"end":{"line":150,"column":41}},"46":{"start":{"line":151,"column":8},"end":{"line":154,"column":10}},"47":{"start":{"line":158,"column":8},"end":{"line":158,"column":41}},"48":{"start":{"line":160,"column":12},"end":{"line":161,"column":null}},"49":{"start":{"line":163,"column":8},"end":{"line":163,"column":58}},"50":{"start":{"line":163,"column":36},"end":{"line":163,"column":56}},"51":{"start":{"line":169,"column":22},"end":{"line":169,"column":79}},"52":{"start":{"line":170,"column":8},"end":{"line":170,"column":79}},"53":{"start":{"line":170,"column":20},"end":{"line":170,"column":79}},"54":{"start":{"line":32,"column":13},"end":{"line":32,"column":30}},"55":{"start":{"line":32,"column":13},"end":{"line":172,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":33,"column":4},"end":{"line":33,"column":null}},"loc":{"start":{"line":42,"column":51},"end":{"line":43,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":45,"column":4},"end":{"line":45,"column":9}},"loc":{"start":{"line":45,"column":49},"end":{"line":52,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":54,"column":4},"end":{"line":54,"column":9}},"loc":{"start":{"line":54,"column":45},"end":{"line":56,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":58,"column":4},"end":{"line":58,"column":9}},"loc":{"start":{"line":58,"column":28},"end":{"line":62,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":64,"column":4},"end":{"line":64,"column":9}},"loc":{"start":{"line":66,"column":38},"end":{"line":77,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":79,"column":4},"end":{"line":79,"column":9}},"loc":{"start":{"line":79,"column":27},"end":{"line":102,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":104,"column":4},"end":{"line":104,"column":9}},"loc":{"start":{"line":106,"column":26},"end":{"line":113,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":115,"column":4},"end":{"line":115,"column":9}},"loc":{"start":{"line":117,"column":26},"end":{"line":121,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":123,"column":4},"end":{"line":123,"column":9}},"loc":{"start":{"line":123,"column":44},"end":{"line":130,"column":5}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":129,"column":29},"end":{"line":129,"column":30}},"loc":{"start":{"line":129,"column":36},"end":{"line":129,"column":48}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":132,"column":4},"end":{"line":132,"column":9}},"loc":{"start":{"line":134,"column":34},"end":{"line":144,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":146,"column":4},"end":{"line":146,"column":9}},"loc":{"start":{"line":148,"column":34},"end":{"line":155,"column":5}}},"12":{"name":"(anonymous_16)","decl":{"start":{"line":157,"column":4},"end":{"line":157,"column":9}},"loc":{"start":{"line":157,"column":52},"end":{"line":164,"column":5}}},"13":{"name":"(anonymous_17)","decl":{"start":{"line":163,"column":29},"end":{"line":163,"column":30}},"loc":{"start":{"line":163,"column":36},"end":{"line":163,"column":56}}},"14":{"name":"(anonymous_18)","decl":{"start":{"line":166,"column":12},"end":{"line":166,"column":17}},"loc":{"start":{"line":167,"column":34},"end":{"line":171,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":47,"column":18},"end":{"line":47,"column":38}},"type":"binary-expr","locations":[{"start":{"line":47,"column":18},"end":{"line":47,"column":30}},{"start":{"line":47,"column":34},"end":{"line":47,"column":38}}]},"1":{"loc":{"start":{"line":49,"column":25},"end":{"line":49,"column":52}},"type":"binary-expr","locations":[{"start":{"line":49,"column":25},"end":{"line":49,"column":44}},{"start":{"line":49,"column":48},"end":{"line":49,"column":52}}]},"2":{"loc":{"start":{"line":60,"column":8},"end":{"line":60,"column":77}},"type":"if","locations":[{"start":{"line":60,"column":8},"end":{"line":60,"column":77}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":70,"column":16},"end":{"line":70,"column":68}},"type":"cond-expr","locations":[{"start":{"line":70,"column":43},"end":{"line":70,"column":63}},{"start":{"line":70,"column":66},"end":{"line":70,"column":68}}]},"4":{"loc":{"start":{"line":71,"column":16},"end":{"line":71,"column":71}},"type":"cond-expr","locations":[{"start":{"line":71,"column":44},"end":{"line":71,"column":66}},{"start":{"line":71,"column":69},"end":{"line":71,"column":71}}]},"5":{"loc":{"start":{"line":72,"column":16},"end":{"line":74,"column":20}},"type":"cond-expr","locations":[{"start":{"line":73,"column":18},"end":{"line":73,"column":52}},{"start":{"line":74,"column":18},"end":{"line":74,"column":20}}]},"6":{"loc":{"start":{"line":83,"column":8},"end":{"line":96,"column":9}},"type":"if","locations":[{"start":{"line":83,"column":8},"end":{"line":96,"column":9}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":84,"column":12},"end":{"line":85,"column":39}},"type":"binary-expr","locations":[{"start":{"line":84,"column":12},"end":{"line":84,"column":47}},{"start":{"line":85,"column":12},"end":{"line":85,"column":39}}]},"8":{"loc":{"start":{"line":170,"column":8},"end":{"line":170,"column":79}},"type":"if","locations":[{"start":{"line":170,"column":8},"end":{"line":170,"column":79}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":35,"column":38},"end":{"line":35,"column":null}},"type":"cond-expr","locations":[{"start":{"line":35,"column":62},"end":{"line":35,"column":null}},{"start":{"line":35,"column":62},"end":{"line":35,"column":null}}]},"10":{"loc":{"start":{"line":35,"column":38},"end":{"line":35,"column":62}},"type":"binary-expr","locations":[{"start":{"line":35,"column":38},"end":{"line":35,"column":null}},{"start":{"line":35,"column":38},"end":{"line":35,"column":62}}]},"11":{"loc":{"start":{"line":35,"column":62},"end":{"line":37,"column":null}},"type":"cond-expr","locations":[{"start":{"line":37,"column":74},"end":{"line":37,"column":null}},{"start":{"line":37,"column":74},"end":{"line":37,"column":null}}]},"12":{"loc":{"start":{"line":35,"column":62},"end":{"line":37,"column":74}},"type":"binary-expr","locations":[{"start":{"line":35,"column":62},"end":{"line":37,"column":null}},{"start":{"line":37,"column":44},"end":{"line":37,"column":74}}]},"13":{"loc":{"start":{"line":37,"column":74},"end":{"line":39,"column":null}},"type":"cond-expr","locations":[{"start":{"line":39,"column":94},"end":{"line":39,"column":null}},{"start":{"line":39,"column":94},"end":{"line":39,"column":null}}]},"14":{"loc":{"start":{"line":37,"column":74},"end":{"line":39,"column":94}},"type":"binary-expr","locations":[{"start":{"line":37,"column":74},"end":{"line":39,"column":null}},{"start":{"line":39,"column":54},"end":{"line":39,"column":94}}]},"15":{"loc":{"start":{"line":39,"column":94},"end":{"line":41,"column":null}},"type":"cond-expr","locations":[{"start":{"line":41,"column":110},"end":{"line":41,"column":null}},{"start":{"line":41,"column":110},"end":{"line":41,"column":null}}]},"16":{"loc":{"start":{"line":39,"column":94},"end":{"line":41,"column":110}},"type":"binary-expr","locations":[{"start":{"line":39,"column":94},"end":{"line":41,"column":null}},{"start":{"line":41,"column":62},"end":{"line":41,"column":110}}]},"17":{"loc":{"start":{"line":41,"column":110},"end":{"line":42,"column":null}},"type":"cond-expr","locations":[{"start":{"line":42,"column":51},"end":{"line":42,"column":null}},{"start":{"line":42,"column":51},"end":{"line":42,"column":null}}]},"18":{"loc":{"start":{"line":41,"column":110},"end":{"line":42,"column":51}},"type":"binary-expr","locations":[{"start":{"line":41,"column":110},"end":{"line":42,"column":null}},{"start":{"line":42,"column":36},"end":{"line":42,"column":51}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":44,"7":44,"8":44,"9":44,"10":44,"11":44,"12":44,"13":102,"14":102,"15":3,"16":114,"17":114,"18":8,"19":106,"20":2,"21":1,"22":1,"23":4,"24":3,"25":3,"26":3,"27":2,"28":1,"29":1,"30":1,"31":5,"32":5,"33":4,"34":4,"35":2,"36":2,"37":8,"38":8,"39":8,"40":5,"41":3,"42":3,"43":2,"44":2,"45":1,"46":1,"47":6,"48":6,"49":6,"50":3,"51":3,"52":3,"53":1,"54":44,"55":44},"f":{"0":44,"1":102,"2":3,"3":114,"4":2,"5":4,"6":5,"7":2,"8":8,"9":5,"10":3,"11":1,"12":6,"13":3,"14":3},"b":{"0":[102,92],"1":[102,91],"2":[8,106],"3":[0,1],"4":[1,0],"5":[0,1],"6":[2,1],"7":[3,2],"8":[1,2],"9":[0,44],"10":[44,0],"11":[0,44],"12":[44,0],"13":[0,44],"14":[44,0],"15":[0,44],"16":[44,0],"17":[44,0],"18":[44,44]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/question-template.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/question-template.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"2":{"start":{"line":13,"column":0},"end":{"line":13,"column":null}},"3":{"start":{"line":17,"column":0},"end":{"line":17,"column":98}},"4":{"start":{"line":18,"column":0},"end":{"line":18,"column":79}},"5":{"start":{"line":19,"column":0},"end":{"line":19,"column":null}},"6":{"start":{"line":23,"column":0},"end":{"line":23,"column":null}},"7":{"start":{"line":27,"column":0},"end":{"line":27,"column":null}},"8":{"start":{"line":31,"column":0},"end":{"line":31,"column":57}},"9":{"start":{"line":34,"column":7},"end":{"line":226,"column":null}},"10":{"start":{"line":37,"column":25},"end":{"line":37,"column":44}},"11":{"start":{"line":39,"column":25},"end":{"line":39,"column":60}},"12":{"start":{"line":41,"column":25},"end":{"line":41,"column":62}},"13":{"start":{"line":42,"column":25},"end":{"line":42,"column":38}},"14":{"start":{"line":43,"column":25},"end":{"line":43,"column":36}},"15":{"start":{"line":45,"column":25},"end":{"line":45,"column":42}},"16":{"start":{"line":51,"column":8},"end":{"line":51,"column":61}},"17":{"start":{"line":53,"column":25},"end":{"line":60,"column":10}},"18":{"start":{"line":62,"column":24},"end":{"line":62,"column":69}},"19":{"start":{"line":64,"column":28},"end":{"line":66,"column":16}},"20":{"start":{"line":67,"column":8},"end":{"line":73,"column":9}},"21":{"start":{"line":68,"column":12},"end":{"line":68,"column":57}},"22":{"start":{"line":69,"column":12},"end":{"line":72,"column":14}},"23":{"start":{"line":75,"column":8},"end":{"line":75,"column":41}},"24":{"start":{"line":81,"column":8},"end":{"line":81,"column":52}},"25":{"start":{"line":85,"column":25},"end":{"line":85,"column":66}},"26":{"start":{"line":86,"column":8},"end":{"line":87,"column":71}},"27":{"start":{"line":87,"column":12},"end":{"line":87,"column":71}},"28":{"start":{"line":88,"column":8},"end":{"line":88,"column":24}},"29":{"start":{"line":95,"column":24},"end":{"line":95,"column":46}},"30":{"start":{"line":97,"column":8},"end":{"line":99,"column":9}},"31":{"start":{"line":98,"column":12},"end":{"line":98,"column":63}},"32":{"start":{"line":101,"column":55},"end":{"line":116,"column":10}},"33":{"start":{"line":118,"column":8},"end":{"line":118,"column":61}},"34":{"start":{"line":120,"column":8},"end":{"line":127,"column":9}},"35":{"start":{"line":121,"column":36},"end":{"line":121,"column":74}},"36":{"start":{"line":122,"column":12},"end":{"line":122,"column":61}},"37":{"start":{"line":123,"column":12},"end":{"line":126,"column":14}},"38":{"start":{"line":129,"column":8},"end":{"line":129,"column":32}},"39":{"start":{"line":133,"column":8},"end":{"line":133,"column":31}},"40":{"start":{"line":134,"column":33},"end":{"line":136,"column":10}},"41":{"start":{"line":137,"column":8},"end":{"line":145,"column":9}},"42":{"start":{"line":138,"column":12},"end":{"line":144,"column":14}},"43":{"start":{"line":146,"column":8},"end":{"line":148,"column":10}},"44":{"start":{"line":149,"column":8},"end":{"line":151,"column":10}},"45":{"start":{"line":152,"column":8},"end":{"line":152,"column":52}},"46":{"start":{"line":159,"column":8},"end":{"line":159,"column":39}},"47":{"start":{"line":160,"column":8},"end":{"line":160,"column":49}},"48":{"start":{"line":162,"column":8},"end":{"line":165,"column":10}},"49":{"start":{"line":166,"column":8},"end":{"line":166,"column":46}},"50":{"start":{"line":173,"column":8},"end":{"line":173,"column":39}},"51":{"start":{"line":174,"column":8},"end":{"line":177,"column":10}},"52":{"start":{"line":181,"column":8},"end":{"line":181,"column":39}},"53":{"start":{"line":183,"column":12},"end":{"line":184,"column":null}},"54":{"start":{"line":186,"column":8},"end":{"line":186,"column":50}},"55":{"start":{"line":186,"column":36},"end":{"line":186,"column":48}},"56":{"start":{"line":193,"column":8},"end":{"line":193,"column":39}},"57":{"start":{"line":194,"column":8},"end":{"line":194,"column":53}},"58":{"start":{"line":196,"column":8},"end":{"line":199,"column":10}},"59":{"start":{"line":200,"column":8},"end":{"line":200,"column":48}},"60":{"start":{"line":207,"column":8},"end":{"line":207,"column":39}},"61":{"start":{"line":208,"column":8},"end":{"line":211,"column":10}},"62":{"start":{"line":215,"column":8},"end":{"line":215,"column":39}},"63":{"start":{"line":217,"column":12},"end":{"line":218,"column":null}},"64":{"start":{"line":220,"column":8},"end":{"line":220,"column":52}},"65":{"start":{"line":220,"column":36},"end":{"line":220,"column":50}},"66":{"start":{"line":224,"column":8},"end":{"line":224,"column":79}},"67":{"start":{"line":224,"column":50},"end":{"line":224,"column":76}},"68":{"start":{"line":34,"column":13},"end":{"line":34,"column":36}},"69":{"start":{"line":34,"column":13},"end":{"line":226,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},"loc":{"start":{"line":45,"column":64},"end":{"line":46,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":48,"column":4},"end":{"line":48,"column":9}},"loc":{"start":{"line":49,"column":46},"end":{"line":76,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":78,"column":4},"end":{"line":78,"column":9}},"loc":{"start":{"line":79,"column":42},"end":{"line":82,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":84,"column":4},"end":{"line":84,"column":9}},"loc":{"start":{"line":84,"column":28},"end":{"line":89,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":91,"column":4},"end":{"line":91,"column":9}},"loc":{"start":{"line":93,"column":44},"end":{"line":130,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":132,"column":4},"end":{"line":132,"column":9}},"loc":{"start":{"line":132,"column":27},"end":{"line":153,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":155,"column":4},"end":{"line":155,"column":9}},"loc":{"start":{"line":157,"column":26},"end":{"line":167,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":169,"column":4},"end":{"line":169,"column":9}},"loc":{"start":{"line":171,"column":26},"end":{"line":178,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":180,"column":4},"end":{"line":180,"column":9}},"loc":{"start":{"line":180,"column":42},"end":{"line":187,"column":5}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":186,"column":29},"end":{"line":186,"column":30}},"loc":{"start":{"line":186,"column":36},"end":{"line":186,"column":48}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":189,"column":4},"end":{"line":189,"column":9}},"loc":{"start":{"line":191,"column":28},"end":{"line":201,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":203,"column":4},"end":{"line":203,"column":9}},"loc":{"start":{"line":205,"column":28},"end":{"line":212,"column":5}}},"12":{"name":"(anonymous_16)","decl":{"start":{"line":214,"column":4},"end":{"line":214,"column":9}},"loc":{"start":{"line":214,"column":44},"end":{"line":221,"column":5}}},"13":{"name":"(anonymous_17)","decl":{"start":{"line":220,"column":29},"end":{"line":220,"column":30}},"loc":{"start":{"line":220,"column":36},"end":{"line":220,"column":50}}},"14":{"name":"(anonymous_18)","decl":{"start":{"line":223,"column":12},"end":{"line":223,"column":17}},"loc":{"start":{"line":223,"column":60},"end":{"line":225,"column":5}}},"15":{"name":"(anonymous_19)","decl":{"start":{"line":224,"column":42},"end":{"line":224,"column":43}},"loc":{"start":{"line":224,"column":50},"end":{"line":224,"column":76}}}},"branchMap":{"0":{"loc":{"start":{"line":57,"column":33},"end":{"line":57,"column":69}},"type":"binary-expr","locations":[{"start":{"line":57,"column":33},"end":{"line":57,"column":60}},{"start":{"line":57,"column":64},"end":{"line":57,"column":69}}]},"1":{"loc":{"start":{"line":58,"column":20},"end":{"line":58,"column":67}},"type":"binary-expr","locations":[{"start":{"line":58,"column":20},"end":{"line":58,"column":34}},{"start":{"line":58,"column":38},"end":{"line":58,"column":67}}]},"2":{"loc":{"start":{"line":59,"column":25},"end":{"line":59,"column":50}},"type":"binary-expr","locations":[{"start":{"line":59,"column":25},"end":{"line":59,"column":44}},{"start":{"line":59,"column":48},"end":{"line":59,"column":50}}]},"3":{"loc":{"start":{"line":64,"column":28},"end":{"line":66,"column":16}},"type":"cond-expr","locations":[{"start":{"line":65,"column":14},"end":{"line":65,"column":54}},{"start":{"line":66,"column":14},"end":{"line":66,"column":16}}]},"4":{"loc":{"start":{"line":67,"column":8},"end":{"line":73,"column":9}},"type":"if","locations":[{"start":{"line":67,"column":8},"end":{"line":73,"column":9}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":86,"column":8},"end":{"line":87,"column":71}},"type":"if","locations":[{"start":{"line":86,"column":8},"end":{"line":87,"column":71}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":97,"column":8},"end":{"line":99,"column":9}},"type":"if","locations":[{"start":{"line":97,"column":8},"end":{"line":99,"column":9}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":97,"column":12},"end":{"line":97,"column":77}},"type":"binary-expr","locations":[{"start":{"line":97,"column":12},"end":{"line":97,"column":30}},{"start":{"line":97,"column":34},"end":{"line":97,"column":77}}]},"8":{"loc":{"start":{"line":102,"column":16},"end":{"line":104,"column":20}},"type":"cond-expr","locations":[{"start":{"line":103,"column":18},"end":{"line":103,"column":54}},{"start":{"line":104,"column":18},"end":{"line":104,"column":20}}]},"9":{"loc":{"start":{"line":105,"column":16},"end":{"line":105,"column":71}},"type":"cond-expr","locations":[{"start":{"line":105,"column":44},"end":{"line":105,"column":66}},{"start":{"line":105,"column":69},"end":{"line":105,"column":71}}]},"10":{"loc":{"start":{"line":106,"column":16},"end":{"line":108,"column":20}},"type":"cond-expr","locations":[{"start":{"line":107,"column":18},"end":{"line":107,"column":50}},{"start":{"line":108,"column":18},"end":{"line":108,"column":20}}]},"11":{"loc":{"start":{"line":109,"column":16},"end":{"line":111,"column":20}},"type":"cond-expr","locations":[{"start":{"line":110,"column":18},"end":{"line":110,"column":68}},{"start":{"line":111,"column":18},"end":{"line":111,"column":20}}]},"12":{"loc":{"start":{"line":112,"column":16},"end":{"line":112,"column":74}},"type":"cond-expr","locations":[{"start":{"line":112,"column":45},"end":{"line":112,"column":69}},{"start":{"line":112,"column":72},"end":{"line":112,"column":74}}]},"13":{"loc":{"start":{"line":113,"column":16},"end":{"line":115,"column":20}},"type":"cond-expr","locations":[{"start":{"line":114,"column":18},"end":{"line":114,"column":52}},{"start":{"line":115,"column":18},"end":{"line":115,"column":20}}]},"14":{"loc":{"start":{"line":120,"column":8},"end":{"line":127,"column":9}},"type":"if","locations":[{"start":{"line":120,"column":8},"end":{"line":127,"column":9}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":137,"column":8},"end":{"line":145,"column":9}},"type":"if","locations":[{"start":{"line":137,"column":8},"end":{"line":145,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":37,"column":44},"end":{"line":37,"column":null}},"type":"cond-expr","locations":[{"start":{"line":37,"column":74},"end":{"line":37,"column":null}},{"start":{"line":37,"column":74},"end":{"line":37,"column":null}}]},"17":{"loc":{"start":{"line":37,"column":44},"end":{"line":37,"column":74}},"type":"binary-expr","locations":[{"start":{"line":37,"column":44},"end":{"line":37,"column":null}},{"start":{"line":37,"column":44},"end":{"line":37,"column":74}}]},"18":{"loc":{"start":{"line":37,"column":74},"end":{"line":39,"column":null}},"type":"cond-expr","locations":[{"start":{"line":39,"column":106},"end":{"line":39,"column":null}},{"start":{"line":39,"column":106},"end":{"line":39,"column":null}}]},"19":{"loc":{"start":{"line":37,"column":74},"end":{"line":39,"column":106}},"type":"binary-expr","locations":[{"start":{"line":37,"column":74},"end":{"line":39,"column":null}},{"start":{"line":39,"column":60},"end":{"line":39,"column":106}}]},"20":{"loc":{"start":{"line":39,"column":106},"end":{"line":41,"column":null}},"type":"cond-expr","locations":[{"start":{"line":41,"column":110},"end":{"line":41,"column":null}},{"start":{"line":41,"column":110},"end":{"line":41,"column":null}}]},"21":{"loc":{"start":{"line":39,"column":106},"end":{"line":41,"column":110}},"type":"binary-expr","locations":[{"start":{"line":39,"column":106},"end":{"line":41,"column":null}},{"start":{"line":41,"column":62},"end":{"line":41,"column":110}}]},"22":{"loc":{"start":{"line":41,"column":110},"end":{"line":42,"column":null}},"type":"cond-expr","locations":[{"start":{"line":42,"column":55},"end":{"line":42,"column":null}},{"start":{"line":42,"column":55},"end":{"line":42,"column":null}}]},"23":{"loc":{"start":{"line":41,"column":110},"end":{"line":42,"column":55}},"type":"binary-expr","locations":[{"start":{"line":41,"column":110},"end":{"line":42,"column":null}},{"start":{"line":42,"column":38},"end":{"line":42,"column":55}}]},"24":{"loc":{"start":{"line":42,"column":55},"end":{"line":43,"column":null}},"type":"cond-expr","locations":[{"start":{"line":43,"column":51},"end":{"line":43,"column":null}},{"start":{"line":43,"column":51},"end":{"line":43,"column":null}}]},"25":{"loc":{"start":{"line":42,"column":55},"end":{"line":43,"column":51}},"type":"binary-expr","locations":[{"start":{"line":42,"column":55},"end":{"line":43,"column":null}},{"start":{"line":43,"column":36},"end":{"line":43,"column":51}}]},"26":{"loc":{"start":{"line":43,"column":51},"end":{"line":45,"column":null}},"type":"cond-expr","locations":[{"start":{"line":45,"column":64},"end":{"line":45,"column":null}},{"start":{"line":45,"column":64},"end":{"line":45,"column":null}}]},"27":{"loc":{"start":{"line":43,"column":51},"end":{"line":45,"column":64}},"type":"binary-expr","locations":[{"start":{"line":43,"column":51},"end":{"line":45,"column":null}},{"start":{"line":45,"column":42},"end":{"line":45,"column":64}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":44,"7":44,"8":44,"9":44,"10":44,"11":44,"12":44,"13":44,"14":44,"15":44,"16":47,"17":46,"18":46,"19":45,"20":45,"21":1,"22":1,"23":45,"24":1,"25":68,"26":68,"27":1,"28":67,"29":2,"30":2,"31":1,"32":1,"33":1,"34":1,"35":0,"36":0,"37":0,"38":1,"39":2,"40":2,"41":2,"42":1,"43":1,"44":1,"45":1,"46":3,"47":3,"48":2,"49":2,"50":1,"51":1,"52":3,"53":3,"54":3,"55":2,"56":3,"57":3,"58":2,"59":2,"60":1,"61":1,"62":3,"63":3,"64":3,"65":2,"66":1,"67":2,"68":44,"69":44},"f":{"0":44,"1":47,"2":1,"3":68,"4":2,"5":2,"6":3,"7":1,"8":3,"9":2,"10":3,"11":1,"12":3,"13":2,"14":1,"15":2},"b":{"0":[46,46],"1":[46,46],"2":[46,44],"3":[1,44],"4":[1,44],"5":[1,67],"6":[1,1],"7":[2,1],"8":[0,1],"9":[1,0],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[1,1],"16":[0,44],"17":[44,0],"18":[0,44],"19":[44,0],"20":[0,44],"21":[44,0],"22":[44,0],"23":[44,44],"24":[44,0],"25":[44,44],"26":[0,44],"27":[44,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/cluster.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/cluster.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":12,"column":0},"end":{"line":12,"column":58}},"4":{"start":{"line":15,"column":7},"end":{"line":102,"column":null}},"5":{"start":{"line":16,"column":33},"end":{"line":16,"column":41}},"6":{"start":{"line":19,"column":24},"end":{"line":21,"column":10}},"7":{"start":{"line":23,"column":8},"end":{"line":23,"column":47}},"8":{"start":{"line":27,"column":24},"end":{"line":27,"column":79}},"9":{"start":{"line":28,"column":8},"end":{"line":28,"column":64}},"10":{"start":{"line":32,"column":22},"end":{"line":32,"column":44}},"11":{"start":{"line":33,"column":24},"end":{"line":33,"column":46}},"12":{"start":{"line":35,"column":22},"end":{"line":35,"column":76}},"13":{"start":{"line":36,"column":8},"end":{"line":36,"column":49}},"14":{"start":{"line":43,"column":24},"end":{"line":46,"column":10}},"15":{"start":{"line":48,"column":8},"end":{"line":48,"column":47}},"16":{"start":{"line":52,"column":8},"end":{"line":52,"column":60}},"17":{"start":{"line":63,"column":12},"end":{"line":63,"column":17}},"18":{"start":{"line":64,"column":8},"end":{"line":92,"column":10}},"19":{"start":{"line":98,"column":22},"end":{"line":98,"column":57}},"20":{"start":{"line":99,"column":26},"end":{"line":99,"column":66}},"21":{"start":{"line":100,"column":8},"end":{"line":100,"column":74}},"22":{"start":{"line":15,"column":13},"end":{"line":15,"column":30}},"23":{"start":{"line":15,"column":13},"end":{"line":102,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":33}},"loc":{"start":{"line":16,"column":54},"end":{"line":16,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":18,"column":39},"end":{"line":24,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":26,"column":4},"end":{"line":26,"column":9}},"loc":{"start":{"line":26,"column":29},"end":{"line":29,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":31,"column":4},"end":{"line":31,"column":9}},"loc":{"start":{"line":31,"column":42},"end":{"line":37,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":39,"column":4},"end":{"line":39,"column":9}},"loc":{"start":{"line":41,"column":35},"end":{"line":49,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":51,"column":4},"end":{"line":51,"column":9}},"loc":{"start":{"line":51,"column":31},"end":{"line":53,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":55,"column":12},"end":{"line":55,"column":22}},"loc":{"start":{"line":55,"column":48},"end":{"line":93,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":95,"column":12},"end":{"line":95,"column":22}},"loc":{"start":{"line":96,"column":33},"end":{"line":101,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":28,"column":15},"end":{"line":28,"column":63}},"type":"cond-expr","locations":[{"start":{"line":28,"column":25},"end":{"line":28,"column":56}},{"start":{"line":28,"column":59},"end":{"line":28,"column":63}}]},"1":{"loc":{"start":{"line":65,"column":16},"end":{"line":65,"column":52}},"type":"cond-expr","locations":[{"start":{"line":65,"column":31},"end":{"line":65,"column":47}},{"start":{"line":65,"column":50},"end":{"line":65,"column":52}}]},"2":{"loc":{"start":{"line":66,"column":16},"end":{"line":66,"column":48}},"type":"cond-expr","locations":[{"start":{"line":66,"column":29},"end":{"line":66,"column":43}},{"start":{"line":66,"column":46},"end":{"line":66,"column":48}}]},"3":{"loc":{"start":{"line":67,"column":16},"end":{"line":67,"column":48}},"type":"cond-expr","locations":[{"start":{"line":67,"column":29},"end":{"line":67,"column":43}},{"start":{"line":67,"column":46},"end":{"line":67,"column":48}}]},"4":{"loc":{"start":{"line":68,"column":16},"end":{"line":70,"column":20}},"type":"cond-expr","locations":[{"start":{"line":69,"column":18},"end":{"line":69,"column":69}},{"start":{"line":70,"column":18},"end":{"line":70,"column":20}}]},"5":{"loc":{"start":{"line":71,"column":16},"end":{"line":78,"column":20}},"type":"cond-expr","locations":[{"start":{"line":72,"column":18},"end":{"line":77,"column":null}},{"start":{"line":78,"column":18},"end":{"line":78,"column":20}}]},"6":{"loc":{"start":{"line":79,"column":16},"end":{"line":91,"column":20}},"type":"cond-expr","locations":[{"start":{"line":80,"column":18},"end":{"line":90,"column":null}},{"start":{"line":91,"column":18},"end":{"line":91,"column":20}}]},"7":{"loc":{"start":{"line":98,"column":22},"end":{"line":98,"column":57}},"type":"binary-expr","locations":[{"start":{"line":98,"column":22},"end":{"line":98,"column":34}},{"start":{"line":98,"column":38},"end":{"line":98,"column":57}}]},"8":{"loc":{"start":{"line":99,"column":26},"end":{"line":99,"column":66}},"type":"binary-expr","locations":[{"start":{"line":99,"column":26},"end":{"line":99,"column":45}},{"start":{"line":99,"column":49},"end":{"line":99,"column":66}}]},"9":{"loc":{"start":{"line":16,"column":41},"end":{"line":16,"column":57}},"type":"cond-expr","locations":[{"start":{"line":16,"column":54},"end":{"line":16,"column":57}},{"start":{"line":16,"column":54},"end":{"line":16,"column":57}}]},"10":{"loc":{"start":{"line":16,"column":41},"end":{"line":16,"column":54}},"type":"binary-expr","locations":[{"start":{"line":16,"column":41},"end":{"line":16,"column":57}},{"start":{"line":16,"column":41},"end":{"line":16,"column":54}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":56,"7":56,"8":9,"9":9,"10":12,"11":12,"12":12,"13":12,"14":2,"15":2,"16":2,"17":12,"18":12,"19":12,"20":12,"21":12,"22":44,"23":44},"f":{"0":44,"1":56,"2":9,"3":12,"4":2,"5":2,"6":12,"7":12},"b":{"0":[6,3],"1":[2,10],"2":[0,12],"3":[0,12],"4":[1,11],"5":[0,12],"6":[0,12],"7":[12,11],"8":[12,11],"9":[44,0],"10":[44,44]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/competence-question-template-relation.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/competence-question-template-relation.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":41}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":44}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":60}},"3":{"start":{"line":6,"column":0},"end":{"line":6,"column":115}},"4":{"start":{"line":9,"column":7},"end":{"line":123,"column":null}},"5":{"start":{"line":10,"column":33},"end":{"line":10,"column":41}},"6":{"start":{"line":16,"column":8},"end":{"line":44,"column":9}},"7":{"start":{"line":18,"column":16},"end":{"line":20,"column":18}},"8":{"start":{"line":21,"column":12},"end":{"line":21,"column":79}},"9":{"start":{"line":23,"column":12},"end":{"line":42,"column":13}},"10":{"start":{"line":28,"column":20},"end":{"line":36,"column":null}},"11":{"start":{"line":38,"column":16},"end":{"line":41,"column":22}},"12":{"start":{"line":39,"column":20},"end":{"line":41,"column":22}},"13":{"start":{"line":43,"column":12},"end":{"line":43,"column":24}},"14":{"start":{"line":51,"column":8},"end":{"line":53,"column":11}},"15":{"start":{"line":60,"column":12},"end":{"line":63,"column":14}},"16":{"start":{"line":64,"column":8},"end":{"line":64,"column":80}},"17":{"start":{"line":71,"column":12},"end":{"line":74,"column":14}},"18":{"start":{"line":75,"column":8},"end":{"line":75,"column":80}},"19":{"start":{"line":82,"column":23},"end":{"line":82,"column":63}},"20":{"start":{"line":84,"column":8},"end":{"line":105,"column":11}},"21":{"start":{"line":86,"column":16},"end":{"line":91,"column":38}},"22":{"start":{"line":92,"column":12},"end":{"line":94,"column":15}},"23":{"start":{"line":96,"column":12},"end":{"line":104,"column":13}},"24":{"start":{"line":97,"column":16},"end":{"line":103,"column":19}},"25":{"start":{"line":98,"column":62},"end":{"line":101,"column":22}},"26":{"start":{"line":107,"column":8},"end":{"line":107,"column":51}},"27":{"start":{"line":113,"column":8},"end":{"line":115,"column":11}},"28":{"start":{"line":119,"column":8},"end":{"line":121,"column":11}},"29":{"start":{"line":9,"column":13},"end":{"line":9,"column":57}},"30":{"start":{"line":9,"column":13},"end":{"line":123,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":10,"column":4},"end":{"line":10,"column":33}},"loc":{"start":{"line":10,"column":54},"end":{"line":10,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":9}},"loc":{"start":{"line":14,"column":34},"end":{"line":45,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":47,"column":4},"end":{"line":47,"column":9}},"loc":{"start":{"line":49,"column":34},"end":{"line":54,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":56,"column":4},"end":{"line":56,"column":9}},"loc":{"start":{"line":57,"column":28},"end":{"line":65,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":67,"column":4},"end":{"line":67,"column":9}},"loc":{"start":{"line":68,"column":34},"end":{"line":76,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":78,"column":4},"end":{"line":78,"column":9}},"loc":{"start":{"line":80,"column":37},"end":{"line":108,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":84,"column":39},"end":{"line":84,"column":44}},"loc":{"start":{"line":84,"column":52},"end":{"line":105,"column":9}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":98,"column":37},"end":{"line":98,"column":38}},"loc":{"start":{"line":98,"column":62},"end":{"line":101,"column":22}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":110,"column":4},"end":{"line":110,"column":9}},"loc":{"start":{"line":111,"column":34},"end":{"line":116,"column":5}}},"9":{"name":"(anonymous_11)","decl":{"start":{"line":118,"column":4},"end":{"line":118,"column":9}},"loc":{"start":{"line":118,"column":53},"end":{"line":122,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":23,"column":12},"end":{"line":42,"column":13}},"type":"if","locations":[{"start":{"line":23,"column":12},"end":{"line":42,"column":13}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":24,"column":16},"end":{"line":25,"column":38}},"type":"binary-expr","locations":[{"start":{"line":24,"column":16},"end":{"line":24,"column":69}},{"start":{"line":25,"column":16},"end":{"line":25,"column":38}}]},"2":{"loc":{"start":{"line":38,"column":16},"end":{"line":41,"column":22}},"type":"if","locations":[{"start":{"line":38,"column":16},"end":{"line":41,"column":22}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":86,"column":16},"end":{"line":91,"column":38}},"type":"cond-expr","locations":[{"start":{"line":87,"column":22},"end":{"line":90,"column":null}},{"start":{"line":91,"column":22},"end":{"line":91,"column":38}}]},"4":{"loc":{"start":{"line":96,"column":12},"end":{"line":104,"column":13}},"type":"if","locations":[{"start":{"line":96,"column":12},"end":{"line":104,"column":13}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":10,"column":41},"end":{"line":10,"column":57}},"type":"cond-expr","locations":[{"start":{"line":10,"column":54},"end":{"line":10,"column":57}},{"start":{"line":10,"column":54},"end":{"line":10,"column":57}}]},"6":{"loc":{"start":{"line":10,"column":41},"end":{"line":10,"column":54}},"type":"binary-expr","locations":[{"start":{"line":10,"column":41},"end":{"line":10,"column":57}},{"start":{"line":10,"column":41},"end":{"line":10,"column":54}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":14,"7":14,"8":13,"9":1,"10":1,"11":1,"12":1,"13":0,"14":3,"15":8,"16":8,"17":5,"18":5,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":2,"28":2,"29":44,"30":44},"f":{"0":44,"1":14,"2":3,"3":8,"4":5,"5":0,"6":0,"7":0,"8":2,"9":2},"b":{"0":[1,0],"1":[1,1],"2":[1,0],"3":[0,0],"4":[0,0],"5":[44,0],"6":[44,44]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/competence.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/competence.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":12,"column":0},"end":{"line":12,"column":64}},"4":{"start":{"line":15,"column":7},"end":{"line":98,"column":null}},"5":{"start":{"line":16,"column":33},"end":{"line":16,"column":41}},"6":{"start":{"line":19,"column":24},"end":{"line":21,"column":10}},"7":{"start":{"line":23,"column":8},"end":{"line":23,"column":50}},"8":{"start":{"line":27,"column":27},"end":{"line":29,"column":10}},"9":{"start":{"line":30,"column":8},"end":{"line":30,"column":73}},"10":{"start":{"line":34,"column":22},"end":{"line":34,"column":44}},"11":{"start":{"line":35,"column":24},"end":{"line":35,"column":46}},"12":{"start":{"line":37,"column":22},"end":{"line":37,"column":79}},"13":{"start":{"line":38,"column":8},"end":{"line":38,"column":52}},"14":{"start":{"line":45,"column":24},"end":{"line":48,"column":10}},"15":{"start":{"line":50,"column":8},"end":{"line":50,"column":50}},"16":{"start":{"line":54,"column":8},"end":{"line":54,"column":63}},"17":{"start":{"line":60,"column":53},"end":{"line":60,"column":58}},"18":{"start":{"line":61,"column":8},"end":{"line":88,"column":10}},"19":{"start":{"line":94,"column":22},"end":{"line":94,"column":60}},"20":{"start":{"line":95,"column":26},"end":{"line":95,"column":66}},"21":{"start":{"line":96,"column":8},"end":{"line":96,"column":74}},"22":{"start":{"line":15,"column":13},"end":{"line":15,"column":33}},"23":{"start":{"line":15,"column":13},"end":{"line":98,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":33}},"loc":{"start":{"line":16,"column":54},"end":{"line":16,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":18,"column":45},"end":{"line":24,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":26,"column":4},"end":{"line":26,"column":9}},"loc":{"start":{"line":26,"column":29},"end":{"line":31,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":33,"column":4},"end":{"line":33,"column":9}},"loc":{"start":{"line":33,"column":45},"end":{"line":39,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":41,"column":4},"end":{"line":41,"column":9}},"loc":{"start":{"line":43,"column":38},"end":{"line":51,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":53,"column":4},"end":{"line":53,"column":9}},"loc":{"start":{"line":53,"column":31},"end":{"line":55,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":57,"column":12},"end":{"line":57,"column":22}},"loc":{"start":{"line":58,"column":36},"end":{"line":89,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":91,"column":12},"end":{"line":91,"column":22}},"loc":{"start":{"line":92,"column":36},"end":{"line":97,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":30,"column":15},"end":{"line":30,"column":72}},"type":"cond-expr","locations":[{"start":{"line":30,"column":28},"end":{"line":30,"column":65}},{"start":{"line":30,"column":68},"end":{"line":30,"column":72}}]},"1":{"loc":{"start":{"line":62,"column":16},"end":{"line":62,"column":77}},"type":"cond-expr","locations":[{"start":{"line":62,"column":23},"end":{"line":62,"column":72}},{"start":{"line":62,"column":75},"end":{"line":62,"column":77}}]},"2":{"loc":{"start":{"line":63,"column":16},"end":{"line":65,"column":20}},"type":"cond-expr","locations":[{"start":{"line":64,"column":18},"end":{"line":64,"column":69}},{"start":{"line":65,"column":18},"end":{"line":65,"column":20}}]},"3":{"loc":{"start":{"line":66,"column":16},"end":{"line":73,"column":20}},"type":"cond-expr","locations":[{"start":{"line":67,"column":18},"end":{"line":72,"column":null}},{"start":{"line":73,"column":18},"end":{"line":73,"column":20}}]},"4":{"loc":{"start":{"line":74,"column":16},"end":{"line":87,"column":20}},"type":"cond-expr","locations":[{"start":{"line":75,"column":18},"end":{"line":86,"column":null}},{"start":{"line":87,"column":18},"end":{"line":87,"column":20}}]},"5":{"loc":{"start":{"line":94,"column":22},"end":{"line":94,"column":60}},"type":"binary-expr","locations":[{"start":{"line":94,"column":22},"end":{"line":94,"column":34}},{"start":{"line":94,"column":38},"end":{"line":94,"column":60}}]},"6":{"loc":{"start":{"line":95,"column":26},"end":{"line":95,"column":66}},"type":"binary-expr","locations":[{"start":{"line":95,"column":26},"end":{"line":95,"column":45}},{"start":{"line":95,"column":49},"end":{"line":95,"column":66}}]},"7":{"loc":{"start":{"line":16,"column":41},"end":{"line":16,"column":57}},"type":"cond-expr","locations":[{"start":{"line":16,"column":54},"end":{"line":16,"column":57}},{"start":{"line":16,"column":54},"end":{"line":16,"column":57}}]},"8":{"loc":{"start":{"line":16,"column":41},"end":{"line":16,"column":54}},"type":"binary-expr","locations":[{"start":{"line":16,"column":41},"end":{"line":16,"column":57}},{"start":{"line":16,"column":41},"end":{"line":16,"column":54}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":217,"7":217,"8":118,"9":118,"10":6,"11":6,"12":6,"13":6,"14":2,"15":2,"16":2,"17":6,"18":6,"19":6,"20":6,"21":6,"22":44,"23":44},"f":{"0":44,"1":217,"2":118,"3":6,"4":2,"5":2,"6":6,"7":6},"b":{"0":[109,9],"1":[1,5],"2":[1,5],"3":[0,6],"4":[1,5],"5":[6,5],"6":[6,5],"7":[44,0],"8":[44,44]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/position-competence-relation.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/position-competence-relation.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":41}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":44}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":60}},"3":{"start":{"line":6,"column":0},"end":{"line":6,"column":98}},"4":{"start":{"line":9,"column":7},"end":{"line":107,"column":null}},"5":{"start":{"line":10,"column":33},"end":{"line":10,"column":41}},"6":{"start":{"line":16,"column":8},"end":{"line":40,"column":9}},"7":{"start":{"line":18,"column":16},"end":{"line":20,"column":18}},"8":{"start":{"line":21,"column":12},"end":{"line":21,"column":71}},"9":{"start":{"line":23,"column":12},"end":{"line":38,"column":13}},"10":{"start":{"line":28,"column":20},"end":{"line":35,"column":22}},"11":{"start":{"line":36,"column":16},"end":{"line":37,"column":79}},"12":{"start":{"line":37,"column":20},"end":{"line":37,"column":79}},"13":{"start":{"line":39,"column":12},"end":{"line":39,"column":24}},"14":{"start":{"line":44,"column":8},"end":{"line":46,"column":11}},"15":{"start":{"line":52,"column":26},"end":{"line":56,"column":null}},"16":{"start":{"line":58,"column":8},"end":{"line":58,"column":72}},"17":{"start":{"line":64,"column":26},"end":{"line":68,"column":null}},"18":{"start":{"line":70,"column":8},"end":{"line":70,"column":72}},"19":{"start":{"line":77,"column":23},"end":{"line":77,"column":55}},"20":{"start":{"line":79,"column":8},"end":{"line":97,"column":11}},"21":{"start":{"line":81,"column":16},"end":{"line":83,"column":38}},"22":{"start":{"line":84,"column":12},"end":{"line":86,"column":15}},"23":{"start":{"line":88,"column":12},"end":{"line":96,"column":13}},"24":{"start":{"line":89,"column":16},"end":{"line":95,"column":19}},"25":{"start":{"line":90,"column":54},"end":{"line":93,"column":22}},"26":{"start":{"line":99,"column":8},"end":{"line":99,"column":51}},"27":{"start":{"line":103,"column":8},"end":{"line":105,"column":11}},"28":{"start":{"line":9,"column":13},"end":{"line":9,"column":49}},"29":{"start":{"line":9,"column":13},"end":{"line":107,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":10,"column":4},"end":{"line":10,"column":33}},"loc":{"start":{"line":10,"column":54},"end":{"line":10,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":9}},"loc":{"start":{"line":14,"column":26},"end":{"line":41,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":43,"column":4},"end":{"line":43,"column":9}},"loc":{"start":{"line":43,"column":57},"end":{"line":47,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":49,"column":4},"end":{"line":49,"column":9}},"loc":{"start":{"line":50,"column":28},"end":{"line":59,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":61,"column":4},"end":{"line":61,"column":9}},"loc":{"start":{"line":62,"column":26},"end":{"line":71,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":73,"column":4},"end":{"line":73,"column":9}},"loc":{"start":{"line":75,"column":29},"end":{"line":100,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":79,"column":39},"end":{"line":79,"column":44}},"loc":{"start":{"line":79,"column":52},"end":{"line":97,"column":9}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":90,"column":37},"end":{"line":90,"column":38}},"loc":{"start":{"line":90,"column":54},"end":{"line":93,"column":22}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":102,"column":4},"end":{"line":102,"column":9}},"loc":{"start":{"line":102,"column":53},"end":{"line":106,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":23,"column":12},"end":{"line":38,"column":13}},"type":"if","locations":[{"start":{"line":23,"column":12},"end":{"line":38,"column":13}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":24,"column":16},"end":{"line":25,"column":38}},"type":"binary-expr","locations":[{"start":{"line":24,"column":16},"end":{"line":24,"column":69}},{"start":{"line":25,"column":16},"end":{"line":25,"column":38}}]},"2":{"loc":{"start":{"line":36,"column":16},"end":{"line":37,"column":79}},"type":"if","locations":[{"start":{"line":36,"column":16},"end":{"line":37,"column":79}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":81,"column":16},"end":{"line":83,"column":38}},"type":"cond-expr","locations":[{"start":{"line":82,"column":22},"end":{"line":82,"column":75}},{"start":{"line":83,"column":22},"end":{"line":83,"column":38}}]},"4":{"loc":{"start":{"line":88,"column":12},"end":{"line":96,"column":13}},"type":"if","locations":[{"start":{"line":88,"column":12},"end":{"line":96,"column":13}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":10,"column":41},"end":{"line":10,"column":57}},"type":"cond-expr","locations":[{"start":{"line":10,"column":54},"end":{"line":10,"column":57}},{"start":{"line":10,"column":54},"end":{"line":10,"column":57}}]},"6":{"loc":{"start":{"line":10,"column":41},"end":{"line":10,"column":54}},"type":"binary-expr","locations":[{"start":{"line":10,"column":41},"end":{"line":10,"column":57}},{"start":{"line":10,"column":41},"end":{"line":10,"column":54}}]}},"s":{"0":50,"1":50,"2":50,"3":50,"4":50,"5":50,"6":17,"7":17,"8":15,"9":2,"10":2,"11":2,"12":2,"13":0,"14":4,"15":12,"16":12,"17":5,"18":5,"19":2,"20":2,"21":2,"22":2,"23":2,"24":1,"25":2,"26":2,"27":2,"28":50,"29":50},"f":{"0":50,"1":17,"2":4,"3":12,"4":5,"5":2,"6":2,"7":2,"8":2},"b":{"0":[2,0],"1":[2,2],"2":[2,0],"3":[1,1],"4":[1,1],"5":[50,0],"6":[50,50]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/position-question-template-relation.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/position-question-template-relation.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":41}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":44}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":60}},"3":{"start":{"line":6,"column":0},"end":{"line":6,"column":111}},"4":{"start":{"line":9,"column":7},"end":{"line":104,"column":null}},"5":{"start":{"line":10,"column":33},"end":{"line":10,"column":41}},"6":{"start":{"line":16,"column":8},"end":{"line":44,"column":9}},"7":{"start":{"line":18,"column":16},"end":{"line":20,"column":18}},"8":{"start":{"line":21,"column":12},"end":{"line":21,"column":77}},"9":{"start":{"line":23,"column":12},"end":{"line":42,"column":13}},"10":{"start":{"line":28,"column":20},"end":{"line":36,"column":null}},"11":{"start":{"line":38,"column":16},"end":{"line":41,"column":22}},"12":{"start":{"line":39,"column":20},"end":{"line":41,"column":22}},"13":{"start":{"line":43,"column":12},"end":{"line":43,"column":24}},"14":{"start":{"line":48,"column":8},"end":{"line":50,"column":11}},"15":{"start":{"line":57,"column":12},"end":{"line":60,"column":14}},"16":{"start":{"line":62,"column":8},"end":{"line":62,"column":78}},"17":{"start":{"line":69,"column":23},"end":{"line":69,"column":55}},"18":{"start":{"line":71,"column":8},"end":{"line":92,"column":11}},"19":{"start":{"line":73,"column":16},"end":{"line":78,"column":56}},"20":{"start":{"line":79,"column":12},"end":{"line":81,"column":15}},"21":{"start":{"line":83,"column":12},"end":{"line":91,"column":13}},"22":{"start":{"line":84,"column":16},"end":{"line":90,"column":19}},"23":{"start":{"line":85,"column":54},"end":{"line":88,"column":22}},"24":{"start":{"line":94,"column":8},"end":{"line":94,"column":47}},"25":{"start":{"line":100,"column":8},"end":{"line":102,"column":11}},"26":{"start":{"line":9,"column":13},"end":{"line":9,"column":55}},"27":{"start":{"line":9,"column":13},"end":{"line":104,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":10,"column":4},"end":{"line":10,"column":33}},"loc":{"start":{"line":10,"column":54},"end":{"line":10,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":9}},"loc":{"start":{"line":14,"column":26},"end":{"line":45,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":47,"column":4},"end":{"line":47,"column":9}},"loc":{"start":{"line":47,"column":55},"end":{"line":51,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":53,"column":4},"end":{"line":53,"column":9}},"loc":{"start":{"line":54,"column":26},"end":{"line":63,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":65,"column":4},"end":{"line":65,"column":9}},"loc":{"start":{"line":67,"column":29},"end":{"line":95,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":71,"column":39},"end":{"line":71,"column":44}},"loc":{"start":{"line":71,"column":52},"end":{"line":92,"column":9}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":85,"column":37},"end":{"line":85,"column":38}},"loc":{"start":{"line":85,"column":54},"end":{"line":88,"column":22}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":97,"column":4},"end":{"line":97,"column":9}},"loc":{"start":{"line":98,"column":34},"end":{"line":103,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":23,"column":12},"end":{"line":42,"column":13}},"type":"if","locations":[{"start":{"line":23,"column":12},"end":{"line":42,"column":13}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":24,"column":16},"end":{"line":25,"column":38}},"type":"binary-expr","locations":[{"start":{"line":24,"column":16},"end":{"line":24,"column":69}},{"start":{"line":25,"column":16},"end":{"line":25,"column":38}}]},"2":{"loc":{"start":{"line":38,"column":16},"end":{"line":41,"column":22}},"type":"if","locations":[{"start":{"line":38,"column":16},"end":{"line":41,"column":22}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":73,"column":16},"end":{"line":78,"column":56}},"type":"cond-expr","locations":[{"start":{"line":74,"column":22},"end":{"line":77,"column":null}},{"start":{"line":78,"column":22},"end":{"line":78,"column":56}}]},"4":{"loc":{"start":{"line":83,"column":12},"end":{"line":91,"column":13}},"type":"if","locations":[{"start":{"line":83,"column":12},"end":{"line":91,"column":13}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":10,"column":41},"end":{"line":10,"column":57}},"type":"cond-expr","locations":[{"start":{"line":10,"column":54},"end":{"line":10,"column":57}},{"start":{"line":10,"column":54},"end":{"line":10,"column":57}}]},"6":{"loc":{"start":{"line":10,"column":41},"end":{"line":10,"column":54}},"type":"binary-expr","locations":[{"start":{"line":10,"column":41},"end":{"line":10,"column":57}},{"start":{"line":10,"column":41},"end":{"line":10,"column":54}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":13,"7":13,"8":12,"9":1,"10":1,"11":1,"12":1,"13":0,"14":2,"15":9,"16":9,"17":4,"18":4,"19":4,"20":4,"21":4,"22":3,"23":5,"24":4,"25":2,"26":44,"27":44},"f":{"0":44,"1":13,"2":2,"3":9,"4":4,"5":4,"6":5,"7":2},"b":{"0":[1,0],"1":[1,1],"2":[1,0],"3":[3,1],"4":[3,1],"5":[44,0],"6":[44,44]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/question-template.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/infrastructure/prisma-repositories/question-template.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":12,"column":0},"end":{"line":12,"column":77}},"4":{"start":{"line":15,"column":7},"end":{"line":124,"column":null}},"5":{"start":{"line":16,"column":33},"end":{"line":16,"column":41}},"6":{"start":{"line":21,"column":31},"end":{"line":21,"column":72}},"7":{"start":{"line":22,"column":8},"end":{"line":26,"column":10}},"8":{"start":{"line":23,"column":62},"end":{"line":25,"column":14}},"9":{"start":{"line":27,"column":24},"end":{"line":30,"column":10}},"10":{"start":{"line":32,"column":8},"end":{"line":32,"column":56}},"11":{"start":{"line":36,"column":25},"end":{"line":39,"column":10}},"12":{"start":{"line":41,"column":8},"end":{"line":41,"column":75}},"13":{"start":{"line":47,"column":22},"end":{"line":47,"column":44}},"14":{"start":{"line":48,"column":24},"end":{"line":48,"column":46}},"15":{"start":{"line":50,"column":22},"end":{"line":54,"column":10}},"16":{"start":{"line":56,"column":8},"end":{"line":56,"column":58}},"17":{"start":{"line":63,"column":24},"end":{"line":69,"column":10}},"18":{"start":{"line":71,"column":8},"end":{"line":71,"column":56}},"19":{"start":{"line":75,"column":8},"end":{"line":75,"column":69}},"20":{"start":{"line":88,"column":12},"end":{"line":88,"column":17}},"21":{"start":{"line":90,"column":8},"end":{"line":114,"column":10}},"22":{"start":{"line":120,"column":22},"end":{"line":120,"column":66}},"23":{"start":{"line":121,"column":26},"end":{"line":121,"column":66}},"24":{"start":{"line":122,"column":8},"end":{"line":122,"column":74}},"25":{"start":{"line":15,"column":13},"end":{"line":15,"column":39}},"26":{"start":{"line":15,"column":13},"end":{"line":124,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":33}},"loc":{"start":{"line":16,"column":54},"end":{"line":16,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":19,"column":40},"end":{"line":33,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":23,"column":45},"end":{"line":23,"column":46}},"loc":{"start":{"line":23,"column":62},"end":{"line":25,"column":14}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":9}},"loc":{"start":{"line":35,"column":29},"end":{"line":42,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":44,"column":4},"end":{"line":44,"column":9}},"loc":{"start":{"line":45,"column":42},"end":{"line":57,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":59,"column":4},"end":{"line":59,"column":9}},"loc":{"start":{"line":61,"column":44},"end":{"line":72,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":74,"column":4},"end":{"line":74,"column":9}},"loc":{"start":{"line":74,"column":31},"end":{"line":76,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":78,"column":12},"end":{"line":78,"column":22}},"loc":{"start":{"line":79,"column":42},"end":{"line":115,"column":5}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":117,"column":12},"end":{"line":117,"column":22}},"loc":{"start":{"line":118,"column":42},"end":{"line":123,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":41,"column":15},"end":{"line":41,"column":74}},"type":"cond-expr","locations":[{"start":{"line":41,"column":26},"end":{"line":41,"column":67}},{"start":{"line":41,"column":70},"end":{"line":41,"column":74}}]},"1":{"loc":{"start":{"line":91,"column":16},"end":{"line":93,"column":20}},"type":"cond-expr","locations":[{"start":{"line":92,"column":18},"end":{"line":92,"column":69}},{"start":{"line":93,"column":18},"end":{"line":93,"column":20}}]},"2":{"loc":{"start":{"line":94,"column":16},"end":{"line":94,"column":52}},"type":"cond-expr","locations":[{"start":{"line":94,"column":31},"end":{"line":94,"column":47}},{"start":{"line":94,"column":50},"end":{"line":94,"column":52}}]},"3":{"loc":{"start":{"line":95,"column":16},"end":{"line":95,"column":40}},"type":"cond-expr","locations":[{"start":{"line":95,"column":25},"end":{"line":95,"column":35}},{"start":{"line":95,"column":38},"end":{"line":95,"column":40}}]},"4":{"loc":{"start":{"line":96,"column":16},"end":{"line":96,"column":48}},"type":"cond-expr","locations":[{"start":{"line":96,"column":29},"end":{"line":96,"column":43}},{"start":{"line":96,"column":46},"end":{"line":96,"column":48}}]},"5":{"loc":{"start":{"line":97,"column":16},"end":{"line":106,"column":20}},"type":"cond-expr","locations":[{"start":{"line":98,"column":18},"end":{"line":105,"column":null}},{"start":{"line":106,"column":18},"end":{"line":106,"column":20}}]},"6":{"loc":{"start":{"line":98,"column":18},"end":{"line":105,"column":null}},"type":"cond-expr","locations":[{"start":{"line":99,"column":22},"end":{"line":99,"column":51}},{"start":{"line":100,"column":22},"end":{"line":105,"column":null}}]},"7":{"loc":{"start":{"line":107,"column":16},"end":{"line":113,"column":20}},"type":"cond-expr","locations":[{"start":{"line":108,"column":18},"end":{"line":112,"column":null}},{"start":{"line":113,"column":18},"end":{"line":113,"column":20}}]},"8":{"loc":{"start":{"line":120,"column":22},"end":{"line":120,"column":66}},"type":"binary-expr","locations":[{"start":{"line":120,"column":22},"end":{"line":120,"column":34}},{"start":{"line":120,"column":38},"end":{"line":120,"column":66}}]},"9":{"loc":{"start":{"line":121,"column":26},"end":{"line":121,"column":66}},"type":"binary-expr","locations":[{"start":{"line":121,"column":26},"end":{"line":121,"column":45}},{"start":{"line":121,"column":49},"end":{"line":121,"column":66}}]},"10":{"loc":{"start":{"line":16,"column":41},"end":{"line":16,"column":57}},"type":"cond-expr","locations":[{"start":{"line":16,"column":54},"end":{"line":16,"column":57}},{"start":{"line":16,"column":54},"end":{"line":16,"column":57}}]},"11":{"loc":{"start":{"line":16,"column":41},"end":{"line":16,"column":54}},"type":"binary-expr","locations":[{"start":{"line":16,"column":41},"end":{"line":16,"column":57}},{"start":{"line":16,"column":41},"end":{"line":16,"column":54}}]}},"s":{"0":44,"1":44,"2":44,"3":44,"4":44,"5":44,"6":75,"7":75,"8":6,"9":75,"10":74,"11":73,"12":73,"13":5,"14":5,"15":5,"16":5,"17":2,"18":2,"19":2,"20":5,"21":5,"22":5,"23":5,"24":5,"25":44,"26":44},"f":{"0":44,"1":75,"2":6,"3":73,"4":5,"5":2,"6":2,"7":5,"8":5},"b":{"0":[70,3],"1":[0,5],"2":[1,4],"3":[1,4],"4":[1,4],"5":[0,5],"6":[0,0],"7":[0,5],"8":[5,4],"9":[5,4],"10":[44,0],"11":[44,44]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/cycle-stage-notification.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/cycle-stage-notification.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":50}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":52}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":48}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":111}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":95}},"5":{"start":{"line":8,"column":43},"end":{"line":43,"column":null}},"6":{"start":{"line":9,"column":30},"end":{"line":9,"column":77}},"7":{"start":{"line":12,"column":25},"end":{"line":12,"column":40}},"8":{"start":{"line":17,"column":37},"end":{"line":17,"column":42}},"9":{"start":{"line":19,"column":8},"end":{"line":41,"column":9}},"10":{"start":{"line":20,"column":12},"end":{"line":33,"column":13}},"11":{"start":{"line":23,"column":24},"end":{"line":24,"column":null}},"12":{"start":{"line":26,"column":20},"end":{"line":28,"column":22}},"13":{"start":{"line":29,"column":20},"end":{"line":29,"column":27}},"14":{"start":{"line":32,"column":20},"end":{"line":32,"column":27}},"15":{"start":{"line":36,"column":16},"end":{"line":36,"column":70}},"16":{"start":{"line":37,"column":12},"end":{"line":40,"column":14}},"17":{"start":{"line":8,"column":13},"end":{"line":8,"column":43}},"18":{"start":{"line":16,"column":10},"end":{"line":42,"column":null}},"19":{"start":{"line":8,"column":13},"end":{"line":43,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":11,"column":4},"end":{"line":11,"column":null}},"loc":{"start":{"line":12,"column":70},"end":{"line":13,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":16,"column":10},"end":{"line":16,"column":16}},"loc":{"start":{"line":16,"column":46},"end":{"line":42,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":20,"column":12},"end":{"line":33,"column":13}},"type":"switch","locations":[{"start":{"line":21,"column":16},"end":{"line":30,"column":17}},{"start":{"line":31,"column":16},"end":{"line":32,"column":27}}]},"1":{"loc":{"start":{"line":36,"column":16},"end":{"line":36,"column":70}},"type":"cond-expr","locations":[{"start":{"line":36,"column":41},"end":{"line":36,"column":54}},{"start":{"line":36,"column":57},"end":{"line":36,"column":70}}]},"2":{"loc":{"start":{"line":39,"column":16},"end":{"line":39,"column":64}},"type":"cond-expr","locations":[{"start":{"line":39,"column":41},"end":{"line":39,"column":52}},{"start":{"line":39,"column":55},"end":{"line":39,"column":64}}]},"3":{"loc":{"start":{"line":16,"column":24},"end":{"line":16,"column":49}},"type":"cond-expr","locations":[{"start":{"line":16,"column":46},"end":{"line":16,"column":49}},{"start":{"line":16,"column":46},"end":{"line":16,"column":49}}]},"4":{"loc":{"start":{"line":16,"column":24},"end":{"line":16,"column":46}},"type":"binary-expr","locations":[{"start":{"line":16,"column":24},"end":{"line":16,"column":49}},{"start":{"line":16,"column":24},"end":{"line":16,"column":46}}]},"5":{"loc":{"start":{"line":16,"column":49},"end":{"line":16,"column":null}},"type":"cond-expr","locations":[{"start":{"line":16,"column":56},"end":{"line":16,"column":null}},{"start":{"line":16,"column":56},"end":{"line":16,"column":null}}]},"6":{"loc":{"start":{"line":16,"column":49},"end":{"line":16,"column":56}},"type":"binary-expr","locations":[{"start":{"line":16,"column":49},"end":{"line":16,"column":null}},{"start":{"line":16,"column":49},"end":{"line":16,"column":56}}]},"7":{"loc":{"start":{"line":12,"column":40},"end":{"line":12,"column":null}},"type":"cond-expr","locations":[{"start":{"line":12,"column":70},"end":{"line":12,"column":null}},{"start":{"line":12,"column":70},"end":{"line":12,"column":null}}]},"8":{"loc":{"start":{"line":12,"column":40},"end":{"line":12,"column":70}},"type":"binary-expr","locations":[{"start":{"line":12,"column":40},"end":{"line":12,"column":null}},{"start":{"line":12,"column":40},"end":{"line":12,"column":70}}]}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":3,"6":2,"7":2,"8":3,"9":3,"10":3,"11":2,"12":2,"13":2,"14":1,"15":0,"16":0,"17":3,"18":3,"19":3},"f":{"0":2,"1":3},"b":{"0":[2,1],"1":[0,0],"2":[0,0],"3":[3,0],"4":[3,3],"5":[3,0],"6":[3,3],"7":[3,0],"8":[3,3]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/review-stage-notification.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/review-stage-notification.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":51}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":52}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":48}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":113}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":95}},"5":{"start":{"line":8,"column":44},"end":{"line":67,"column":null}},"6":{"start":{"line":9,"column":30},"end":{"line":9,"column":78}},"7":{"start":{"line":12,"column":25},"end":{"line":12,"column":40}},"8":{"start":{"line":17,"column":38},"end":{"line":17,"column":43}},"9":{"start":{"line":19,"column":8},"end":{"line":65,"column":9}},"10":{"start":{"line":20,"column":12},"end":{"line":57,"column":13}},"11":{"start":{"line":23,"column":24},"end":{"line":24,"column":null}},"12":{"start":{"line":26,"column":20},"end":{"line":28,"column":22}},"13":{"start":{"line":29,"column":20},"end":{"line":29,"column":27}},"14":{"start":{"line":33,"column":24},"end":{"line":33,"column":76}},"15":{"start":{"line":34,"column":20},"end":{"line":36,"column":22}},"16":{"start":{"line":37,"column":20},"end":{"line":37,"column":27}},"17":{"start":{"line":41,"column":24},"end":{"line":41,"column":78}},"18":{"start":{"line":42,"column":20},"end":{"line":44,"column":22}},"19":{"start":{"line":45,"column":20},"end":{"line":45,"column":27}},"20":{"start":{"line":49,"column":24},"end":{"line":49,"column":74}},"21":{"start":{"line":50,"column":20},"end":{"line":52,"column":22}},"22":{"start":{"line":53,"column":20},"end":{"line":53,"column":27}},"23":{"start":{"line":56,"column":20},"end":{"line":56,"column":27}},"24":{"start":{"line":60,"column":16},"end":{"line":60,"column":70}},"25":{"start":{"line":61,"column":12},"end":{"line":64,"column":14}},"26":{"start":{"line":8,"column":13},"end":{"line":8,"column":44}},"27":{"start":{"line":16,"column":10},"end":{"line":66,"column":null}},"28":{"start":{"line":8,"column":13},"end":{"line":67,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":11,"column":4},"end":{"line":11,"column":null}},"loc":{"start":{"line":12,"column":70},"end":{"line":13,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":16,"column":10},"end":{"line":16,"column":16}},"loc":{"start":{"line":16,"column":47},"end":{"line":66,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":20,"column":12},"end":{"line":57,"column":13}},"type":"switch","locations":[{"start":{"line":21,"column":16},"end":{"line":30,"column":17}},{"start":{"line":31,"column":16},"end":{"line":38,"column":17}},{"start":{"line":39,"column":16},"end":{"line":46,"column":17}},{"start":{"line":47,"column":16},"end":{"line":54,"column":17}},{"start":{"line":55,"column":16},"end":{"line":56,"column":27}}]},"1":{"loc":{"start":{"line":60,"column":16},"end":{"line":60,"column":70}},"type":"cond-expr","locations":[{"start":{"line":60,"column":41},"end":{"line":60,"column":54}},{"start":{"line":60,"column":57},"end":{"line":60,"column":70}}]},"2":{"loc":{"start":{"line":63,"column":16},"end":{"line":63,"column":64}},"type":"cond-expr","locations":[{"start":{"line":63,"column":41},"end":{"line":63,"column":52}},{"start":{"line":63,"column":55},"end":{"line":63,"column":64}}]},"3":{"loc":{"start":{"line":16,"column":24},"end":{"line":16,"column":50}},"type":"cond-expr","locations":[{"start":{"line":16,"column":47},"end":{"line":16,"column":50}},{"start":{"line":16,"column":47},"end":{"line":16,"column":50}}]},"4":{"loc":{"start":{"line":16,"column":24},"end":{"line":16,"column":47}},"type":"binary-expr","locations":[{"start":{"line":16,"column":24},"end":{"line":16,"column":50}},{"start":{"line":16,"column":24},"end":{"line":16,"column":47}}]},"5":{"loc":{"start":{"line":16,"column":50},"end":{"line":16,"column":null}},"type":"cond-expr","locations":[{"start":{"line":16,"column":57},"end":{"line":16,"column":null}},{"start":{"line":16,"column":57},"end":{"line":16,"column":null}}]},"6":{"loc":{"start":{"line":16,"column":50},"end":{"line":16,"column":57}},"type":"binary-expr","locations":[{"start":{"line":16,"column":50},"end":{"line":16,"column":null}},{"start":{"line":16,"column":50},"end":{"line":16,"column":57}}]},"7":{"loc":{"start":{"line":12,"column":40},"end":{"line":12,"column":null}},"type":"cond-expr","locations":[{"start":{"line":12,"column":70},"end":{"line":12,"column":null}},{"start":{"line":12,"column":70},"end":{"line":12,"column":null}}]},"8":{"loc":{"start":{"line":12,"column":40},"end":{"line":12,"column":70}},"type":"binary-expr","locations":[{"start":{"line":12,"column":40},"end":{"line":12,"column":null}},{"start":{"line":12,"column":40},"end":{"line":12,"column":70}}]}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":3,"6":2,"7":2,"8":5,"9":5,"10":5,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"23":1,"24":0,"25":0,"26":3,"27":3,"28":3},"f":{"0":2,"1":5},"b":{"0":[1,1,1,1,1],"1":[0,0],"2":[0,0],"3":[3,0],"4":[3,3],"5":[3,0],"6":[3,3],"7":[3,0],"8":[3,3]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/user-created-notification.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/user-created-notification.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":52}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":48}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":95}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":95}},"4":{"start":{"line":7,"column":44},"end":{"line":32,"column":null}},"5":{"start":{"line":8,"column":30},"end":{"line":8,"column":78}},"6":{"start":{"line":11,"column":25},"end":{"line":11,"column":40}},"7":{"start":{"line":16,"column":27},"end":{"line":16,"column":32}},"8":{"start":{"line":18,"column":8},"end":{"line":30,"column":9}},"9":{"start":{"line":19,"column":25},"end":{"line":19,"column":75}},"10":{"start":{"line":20,"column":12},"end":{"line":22,"column":14}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":70}},"12":{"start":{"line":26,"column":12},"end":{"line":29,"column":14}},"13":{"start":{"line":7,"column":13},"end":{"line":7,"column":44}},"14":{"start":{"line":15,"column":10},"end":{"line":31,"column":null}},"15":{"start":{"line":7,"column":13},"end":{"line":32,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":10,"column":4},"end":{"line":10,"column":null}},"loc":{"start":{"line":11,"column":70},"end":{"line":12,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":15,"column":10},"end":{"line":15,"column":16}},"loc":{"start":{"line":15,"column":40},"end":{"line":31,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":25,"column":16},"end":{"line":25,"column":70}},"type":"cond-expr","locations":[{"start":{"line":25,"column":41},"end":{"line":25,"column":54}},{"start":{"line":25,"column":57},"end":{"line":25,"column":70}}]},"1":{"loc":{"start":{"line":28,"column":16},"end":{"line":28,"column":64}},"type":"cond-expr","locations":[{"start":{"line":28,"column":41},"end":{"line":28,"column":52}},{"start":{"line":28,"column":55},"end":{"line":28,"column":64}}]},"2":{"loc":{"start":{"line":15,"column":24},"end":{"line":15,"column":43}},"type":"cond-expr","locations":[{"start":{"line":15,"column":40},"end":{"line":15,"column":43}},{"start":{"line":15,"column":40},"end":{"line":15,"column":43}}]},"3":{"loc":{"start":{"line":15,"column":24},"end":{"line":15,"column":40}},"type":"binary-expr","locations":[{"start":{"line":15,"column":24},"end":{"line":15,"column":43}},{"start":{"line":15,"column":24},"end":{"line":15,"column":40}}]},"4":{"loc":{"start":{"line":15,"column":43},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":50},"end":{"line":15,"column":null}},{"start":{"line":15,"column":50},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":15,"column":43},"end":{"line":15,"column":50}},"type":"binary-expr","locations":[{"start":{"line":15,"column":43},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":50}}]},"6":{"loc":{"start":{"line":11,"column":40},"end":{"line":11,"column":null}},"type":"cond-expr","locations":[{"start":{"line":11,"column":70},"end":{"line":11,"column":null}},{"start":{"line":11,"column":70},"end":{"line":11,"column":null}}]},"7":{"loc":{"start":{"line":11,"column":40},"end":{"line":11,"column":70}},"type":"binary-expr","locations":[{"start":{"line":11,"column":40},"end":{"line":11,"column":null}},{"start":{"line":11,"column":40},"end":{"line":11,"column":70}}]}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":2,"6":2,"7":2,"8":2,"9":2,"10":2,"11":0,"12":0,"13":3,"14":3,"15":3},"f":{"0":2,"1":2},"b":{"0":[0,0],"1":[0,0],"2":[3,0],"3":[3,3],"4":[3,0],"5":[3,3],"6":[3,0],"7":[3,3]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/services/review-email-notification.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/services/review-email-notification.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":60}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":47}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"4":{"start":{"line":11,"column":0},"end":{"line":11,"column":null}},"5":{"start":{"line":15,"column":0},"end":{"line":15,"column":91}},"6":{"start":{"line":16,"column":0},"end":{"line":16,"column":null}},"7":{"start":{"line":21,"column":0},"end":{"line":21,"column":null}},"8":{"start":{"line":25,"column":0},"end":{"line":25,"column":71}},"9":{"start":{"line":26,"column":0},"end":{"line":26,"column":77}},"10":{"start":{"line":27,"column":0},"end":{"line":27,"column":null}},"11":{"start":{"line":33,"column":0},"end":{"line":33,"column":58}},"12":{"start":{"line":34,"column":0},"end":{"line":34,"column":null}},"13":{"start":{"line":40,"column":43},"end":{"line":393,"column":null}},"14":{"start":{"line":41,"column":30},"end":{"line":41,"column":77}},"15":{"start":{"line":44,"column":41},"end":{"line":44,"column":49}},"16":{"start":{"line":46,"column":25},"end":{"line":46,"column":46}},"17":{"start":{"line":48,"column":25},"end":{"line":48,"column":37}},"18":{"start":{"line":50,"column":25},"end":{"line":50,"column":41}},"19":{"start":{"line":52,"column":25},"end":{"line":52,"column":39}},"20":{"start":{"line":54,"column":25},"end":{"line":54,"column":35}},"21":{"start":{"line":56,"column":25},"end":{"line":56,"column":46}},"22":{"start":{"line":57,"column":25},"end":{"line":57,"column":39}},"23":{"start":{"line":58,"column":25},"end":{"line":58,"column":40}},"24":{"start":{"line":70,"column":21},"end":{"line":70,"column":57}},"25":{"start":{"line":71,"column":8},"end":{"line":74,"column":9}},"26":{"start":{"line":72,"column":12},"end":{"line":72,"column":75}},"27":{"start":{"line":73,"column":12},"end":{"line":73,"column":21}},"28":{"start":{"line":76,"column":21},"end":{"line":86,"column":10}},"29":{"start":{"line":88,"column":8},"end":{"line":88,"column":28}},"30":{"start":{"line":97,"column":22},"end":{"line":97,"column":62}},"31":{"start":{"line":99,"column":12},"end":{"line":99,"column":65}},"32":{"start":{"line":101,"column":19},"end":{"line":101,"column":59}},"33":{"start":{"line":102,"column":8},"end":{"line":105,"column":9}},"34":{"start":{"line":103,"column":12},"end":{"line":103,"column":80}},"35":{"start":{"line":104,"column":12},"end":{"line":104,"column":21}},"36":{"start":{"line":107,"column":21},"end":{"line":120,"column":10}},"37":{"start":{"line":122,"column":8},"end":{"line":122,"column":28}},"38":{"start":{"line":126,"column":23},"end":{"line":126,"column":63}},"39":{"start":{"line":127,"column":8},"end":{"line":130,"column":9}},"40":{"start":{"line":128,"column":12},"end":{"line":128,"column":61}},"41":{"start":{"line":129,"column":12},"end":{"line":129,"column":21}},"42":{"start":{"line":132,"column":22},"end":{"line":132,"column":66}},"43":{"start":{"line":133,"column":8},"end":{"line":138,"column":9}},"44":{"start":{"line":134,"column":12},"end":{"line":136,"column":14}},"45":{"start":{"line":137,"column":12},"end":{"line":137,"column":21}},"46":{"start":{"line":140,"column":21},"end":{"line":150,"column":10}},"47":{"start":{"line":152,"column":8},"end":{"line":152,"column":28}},"48":{"start":{"line":156,"column":23},"end":{"line":156,"column":63}},"49":{"start":{"line":157,"column":8},"end":{"line":160,"column":9}},"50":{"start":{"line":158,"column":12},"end":{"line":158,"column":61}},"51":{"start":{"line":159,"column":12},"end":{"line":159,"column":21}},"52":{"start":{"line":162,"column":28},"end":{"line":164,"column":null}},"53":{"start":{"line":167,"column":24},"end":{"line":167,"column":25}},"54":{"start":{"line":169,"column":8},"end":{"line":195,"column":9}},"55":{"start":{"line":170,"column":25},"end":{"line":170,"column":78}},"56":{"start":{"line":171,"column":12},"end":{"line":176,"column":13}},"57":{"start":{"line":172,"column":16},"end":{"line":174,"column":18}},"58":{"start":{"line":175,"column":16},"end":{"line":175,"column":25}},"59":{"start":{"line":178,"column":25},"end":{"line":190,"column":14}},"60":{"start":{"line":192,"column":12},"end":{"line":194,"column":13}},"61":{"start":{"line":193,"column":16},"end":{"line":193,"column":31}},"62":{"start":{"line":197,"column":8},"end":{"line":197,"column":25}},"63":{"start":{"line":201,"column":23},"end":{"line":201,"column":63}},"64":{"start":{"line":202,"column":8},"end":{"line":205,"column":9}},"65":{"start":{"line":203,"column":12},"end":{"line":203,"column":61}},"66":{"start":{"line":204,"column":12},"end":{"line":204,"column":21}},"67":{"start":{"line":207,"column":19},"end":{"line":207,"column":60}},"68":{"start":{"line":208,"column":8},"end":{"line":213,"column":9}},"69":{"start":{"line":209,"column":12},"end":{"line":211,"column":14}},"70":{"start":{"line":212,"column":12},"end":{"line":212,"column":21}},"71":{"start":{"line":215,"column":21},"end":{"line":228,"column":10}},"72":{"start":{"line":230,"column":8},"end":{"line":230,"column":28}},"73":{"start":{"line":234,"column":23},"end":{"line":234,"column":63}},"74":{"start":{"line":235,"column":8},"end":{"line":238,"column":9}},"75":{"start":{"line":236,"column":12},"end":{"line":236,"column":61}},"76":{"start":{"line":237,"column":12},"end":{"line":237,"column":21}},"77":{"start":{"line":240,"column":26},"end":{"line":240,"column":76}},"78":{"start":{"line":242,"column":24},"end":{"line":242,"column":25}},"79":{"start":{"line":244,"column":8},"end":{"line":271,"column":9}},"80":{"start":{"line":245,"column":25},"end":{"line":245,"column":74}},"81":{"start":{"line":246,"column":12},"end":{"line":251,"column":13}},"82":{"start":{"line":247,"column":16},"end":{"line":249,"column":18}},"83":{"start":{"line":250,"column":16},"end":{"line":250,"column":25}},"84":{"start":{"line":253,"column":25},"end":{"line":266,"column":14}},"85":{"start":{"line":268,"column":12},"end":{"line":270,"column":13}},"86":{"start":{"line":269,"column":16},"end":{"line":269,"column":31}},"87":{"start":{"line":273,"column":8},"end":{"line":273,"column":25}},"88":{"start":{"line":288,"column":73},"end":{"line":288,"column":77}},"89":{"start":{"line":290,"column":8},"end":{"line":293,"column":9}},"90":{"start":{"line":291,"column":12},"end":{"line":291,"column":75}},"91":{"start":{"line":292,"column":12},"end":{"line":292,"column":25}},"92":{"start":{"line":294,"column":8},"end":{"line":297,"column":9}},"93":{"start":{"line":295,"column":12},"end":{"line":295,"column":71}},"94":{"start":{"line":296,"column":12},"end":{"line":296,"column":25}},"95":{"start":{"line":298,"column":8},"end":{"line":303,"column":9}},"96":{"start":{"line":299,"column":12},"end":{"line":301,"column":14}},"97":{"start":{"line":302,"column":12},"end":{"line":302,"column":25}},"98":{"start":{"line":305,"column":27},"end":{"line":305,"column":54}},"99":{"start":{"line":308,"column":8},"end":{"line":325,"column":9}},"100":{"start":{"line":309,"column":12},"end":{"line":313,"column":14}},"101":{"start":{"line":314,"column":15},"end":{"line":325,"column":9}},"102":{"start":{"line":315,"column":12},"end":{"line":319,"column":14}},"103":{"start":{"line":321,"column":12},"end":{"line":324,"column":14}},"104":{"start":{"line":327,"column":8},"end":{"line":332,"column":9}},"105":{"start":{"line":328,"column":12},"end":{"line":330,"column":14}},"106":{"start":{"line":331,"column":12},"end":{"line":331,"column":25}},"107":{"start":{"line":335,"column":12},"end":{"line":344,"column":14}},"108":{"start":{"line":346,"column":24},"end":{"line":353,"column":10}},"109":{"start":{"line":355,"column":8},"end":{"line":384,"column":9}},"110":{"start":{"line":356,"column":12},"end":{"line":369,"column":15}},"111":{"start":{"line":370,"column":12},"end":{"line":370,"column":61}},"112":{"start":{"line":371,"column":12},"end":{"line":373,"column":14}},"113":{"start":{"line":374,"column":12},"end":{"line":374,"column":24}},"114":{"start":{"line":377,"column":16},"end":{"line":377,"column":70}},"115":{"start":{"line":378,"column":12},"end":{"line":381,"column":14}},"116":{"start":{"line":382,"column":12},"end":{"line":382,"column":73}},"117":{"start":{"line":383,"column":12},"end":{"line":383,"column":25}},"118":{"start":{"line":388,"column":21},"end":{"line":388,"column":78}},"119":{"start":{"line":389,"column":28},"end":{"line":389,"column":73}},"120":{"start":{"line":390,"column":28},"end":{"line":390,"column":71}},"121":{"start":{"line":391,"column":8},"end":{"line":391,"column":47}},"122":{"start":{"line":40,"column":13},"end":{"line":40,"column":43}},"123":{"start":{"line":40,"column":13},"end":{"line":393,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":43,"column":4},"end":{"line":43,"column":null}},"loc":{"start":{"line":58,"column":53},"end":{"line":59,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":69,"column":4},"end":{"line":69,"column":9}},"loc":{"start":{"line":69,"column":42},"end":{"line":89,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":96,"column":4},"end":{"line":96,"column":9}},"loc":{"start":{"line":96,"column":57},"end":{"line":123,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":125,"column":4},"end":{"line":125,"column":9}},"loc":{"start":{"line":125,"column":52},"end":{"line":153,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":155,"column":4},"end":{"line":155,"column":9}},"loc":{"start":{"line":155,"column":44},"end":{"line":198,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":200,"column":4},"end":{"line":200,"column":9}},"loc":{"start":{"line":200,"column":46},"end":{"line":231,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":233,"column":4},"end":{"line":233,"column":9}},"loc":{"start":{"line":233,"column":42},"end":{"line":274,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":276,"column":12},"end":{"line":276,"column":17}},"loc":{"start":{"line":287,"column":5},"end":{"line":385,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":387,"column":12},"end":{"line":387,"column":20}},"loc":{"start":{"line":387,"column":33},"end":{"line":392,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":71,"column":8},"end":{"line":74,"column":9}},"type":"if","locations":[{"start":{"line":71,"column":8},"end":{"line":74,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":88,"column":15},"end":{"line":88,"column":27}},"type":"cond-expr","locations":[{"start":{"line":88,"column":22},"end":{"line":88,"column":23}},{"start":{"line":88,"column":26},"end":{"line":88,"column":27}}]},"2":{"loc":{"start":{"line":102,"column":8},"end":{"line":105,"column":9}},"type":"if","locations":[{"start":{"line":102,"column":8},"end":{"line":105,"column":9}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":122,"column":15},"end":{"line":122,"column":27}},"type":"cond-expr","locations":[{"start":{"line":122,"column":22},"end":{"line":122,"column":23}},{"start":{"line":122,"column":26},"end":{"line":122,"column":27}}]},"4":{"loc":{"start":{"line":127,"column":8},"end":{"line":130,"column":9}},"type":"if","locations":[{"start":{"line":127,"column":8},"end":{"line":130,"column":9}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":133,"column":8},"end":{"line":138,"column":9}},"type":"if","locations":[{"start":{"line":133,"column":8},"end":{"line":138,"column":9}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":152,"column":15},"end":{"line":152,"column":27}},"type":"cond-expr","locations":[{"start":{"line":152,"column":22},"end":{"line":152,"column":23}},{"start":{"line":152,"column":26},"end":{"line":152,"column":27}}]},"7":{"loc":{"start":{"line":157,"column":8},"end":{"line":160,"column":9}},"type":"if","locations":[{"start":{"line":157,"column":8},"end":{"line":160,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":171,"column":12},"end":{"line":176,"column":13}},"type":"if","locations":[{"start":{"line":171,"column":12},"end":{"line":176,"column":13}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":192,"column":12},"end":{"line":194,"column":13}},"type":"if","locations":[{"start":{"line":192,"column":12},"end":{"line":194,"column":13}},{"start":{},"end":{}}]},"10":{"loc":{"start":{"line":202,"column":8},"end":{"line":205,"column":9}},"type":"if","locations":[{"start":{"line":202,"column":8},"end":{"line":205,"column":9}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":208,"column":8},"end":{"line":213,"column":9}},"type":"if","locations":[{"start":{"line":208,"column":8},"end":{"line":213,"column":9}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":230,"column":15},"end":{"line":230,"column":27}},"type":"cond-expr","locations":[{"start":{"line":230,"column":22},"end":{"line":230,"column":23}},{"start":{"line":230,"column":26},"end":{"line":230,"column":27}}]},"13":{"loc":{"start":{"line":235,"column":8},"end":{"line":238,"column":9}},"type":"if","locations":[{"start":{"line":235,"column":8},"end":{"line":238,"column":9}},{"start":{},"end":{}}]},"14":{"loc":{"start":{"line":246,"column":12},"end":{"line":251,"column":13}},"type":"if","locations":[{"start":{"line":246,"column":12},"end":{"line":251,"column":13}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":268,"column":12},"end":{"line":270,"column":13}},"type":"if","locations":[{"start":{"line":268,"column":12},"end":{"line":270,"column":13}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":290,"column":8},"end":{"line":293,"column":9}},"type":"if","locations":[{"start":{"line":290,"column":8},"end":{"line":293,"column":9}},{"start":{},"end":{}}]},"17":{"loc":{"start":{"line":294,"column":8},"end":{"line":297,"column":9}},"type":"if","locations":[{"start":{"line":294,"column":8},"end":{"line":297,"column":9}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":298,"column":8},"end":{"line":303,"column":9}},"type":"if","locations":[{"start":{"line":298,"column":8},"end":{"line":303,"column":9}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":308,"column":8},"end":{"line":325,"column":9}},"type":"if","locations":[{"start":{"line":308,"column":8},"end":{"line":325,"column":9}},{"start":{"line":314,"column":15},"end":{"line":325,"column":9}}]},"20":{"loc":{"start":{"line":314,"column":15},"end":{"line":325,"column":9}},"type":"if","locations":[{"start":{"line":314,"column":15},"end":{"line":325,"column":9}},{"start":{"line":320,"column":15},"end":{"line":325,"column":9}}]},"21":{"loc":{"start":{"line":327,"column":8},"end":{"line":332,"column":9}},"type":"if","locations":[{"start":{"line":327,"column":8},"end":{"line":332,"column":9}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":335,"column":12},"end":{"line":344,"column":14}},"type":"binary-expr","locations":[{"start":{"line":335,"column":12},"end":{"line":335,"column":20}},{"start":{"line":336,"column":13},"end":{"line":343,"column":null}}]},"23":{"loc":{"start":{"line":338,"column":30},"end":{"line":338,"column":71}},"type":"cond-expr","locations":[{"start":{"line":338,"column":56},"end":{"line":338,"column":64}},{"start":{"line":338,"column":67},"end":{"line":338,"column":71}}]},"24":{"loc":{"start":{"line":339,"column":29},"end":{"line":339,"column":69}},"type":"cond-expr","locations":[{"start":{"line":339,"column":54},"end":{"line":339,"column":62}},{"start":{"line":339,"column":65},"end":{"line":339,"column":69}}]},"25":{"loc":{"start":{"line":377,"column":16},"end":{"line":377,"column":70}},"type":"cond-expr","locations":[{"start":{"line":377,"column":41},"end":{"line":377,"column":54}},{"start":{"line":377,"column":57},"end":{"line":377,"column":70}}]},"26":{"loc":{"start":{"line":380,"column":16},"end":{"line":380,"column":64}},"type":"cond-expr","locations":[{"start":{"line":380,"column":41},"end":{"line":380,"column":52}},{"start":{"line":380,"column":55},"end":{"line":380,"column":64}}]},"27":{"loc":{"start":{"line":388,"column":21},"end":{"line":388,"column":78}},"type":"binary-expr","locations":[{"start":{"line":388,"column":21},"end":{"line":388,"column":71}},{"start":{"line":388,"column":75},"end":{"line":388,"column":78}}]},"28":{"loc":{"start":{"line":389,"column":28},"end":{"line":389,"column":73}},"type":"cond-expr","locations":[{"start":{"line":389,"column":49},"end":{"line":389,"column":66}},{"start":{"line":389,"column":69},"end":{"line":389,"column":73}}]},"29":{"loc":{"start":{"line":390,"column":28},"end":{"line":390,"column":71}},"type":"cond-expr","locations":[{"start":{"line":390,"column":51},"end":{"line":390,"column":64}},{"start":{"line":390,"column":67},"end":{"line":390,"column":71}}]},"30":{"loc":{"start":{"line":44,"column":49},"end":{"line":44,"column":null}},"type":"cond-expr","locations":[{"start":{"line":44,"column":59},"end":{"line":44,"column":null}},{"start":{"line":44,"column":59},"end":{"line":44,"column":null}}]},"31":{"loc":{"start":{"line":44,"column":49},"end":{"line":44,"column":59}},"type":"binary-expr","locations":[{"start":{"line":44,"column":49},"end":{"line":44,"column":null}},{"start":{"line":44,"column":49},"end":{"line":44,"column":59}}]},"32":{"loc":{"start":{"line":44,"column":59},"end":{"line":46,"column":null}},"type":"cond-expr","locations":[{"start":{"line":46,"column":75},"end":{"line":46,"column":null}},{"start":{"line":46,"column":75},"end":{"line":46,"column":null}}]},"33":{"loc":{"start":{"line":44,"column":59},"end":{"line":46,"column":75}},"type":"binary-expr","locations":[{"start":{"line":44,"column":59},"end":{"line":46,"column":null}},{"start":{"line":46,"column":46},"end":{"line":46,"column":75}}]},"34":{"loc":{"start":{"line":46,"column":75},"end":{"line":48,"column":null}},"type":"cond-expr","locations":[{"start":{"line":48,"column":57},"end":{"line":48,"column":null}},{"start":{"line":48,"column":57},"end":{"line":48,"column":null}}]},"35":{"loc":{"start":{"line":46,"column":75},"end":{"line":48,"column":57}},"type":"binary-expr","locations":[{"start":{"line":46,"column":75},"end":{"line":48,"column":null}},{"start":{"line":48,"column":37},"end":{"line":48,"column":57}}]},"36":{"loc":{"start":{"line":48,"column":57},"end":{"line":50,"column":null}},"type":"cond-expr","locations":[{"start":{"line":50,"column":65},"end":{"line":50,"column":null}},{"start":{"line":50,"column":65},"end":{"line":50,"column":null}}]},"37":{"loc":{"start":{"line":48,"column":57},"end":{"line":50,"column":65}},"type":"binary-expr","locations":[{"start":{"line":48,"column":57},"end":{"line":50,"column":null}},{"start":{"line":50,"column":41},"end":{"line":50,"column":65}}]},"38":{"loc":{"start":{"line":50,"column":65},"end":{"line":52,"column":null}},"type":"cond-expr","locations":[{"start":{"line":52,"column":61},"end":{"line":52,"column":null}},{"start":{"line":52,"column":61},"end":{"line":52,"column":null}}]},"39":{"loc":{"start":{"line":50,"column":65},"end":{"line":52,"column":61}},"type":"binary-expr","locations":[{"start":{"line":50,"column":65},"end":{"line":52,"column":null}},{"start":{"line":52,"column":39},"end":{"line":52,"column":61}}]},"40":{"loc":{"start":{"line":52,"column":61},"end":{"line":54,"column":null}},"type":"cond-expr","locations":[{"start":{"line":54,"column":53},"end":{"line":54,"column":null}},{"start":{"line":54,"column":53},"end":{"line":54,"column":null}}]},"41":{"loc":{"start":{"line":52,"column":61},"end":{"line":54,"column":53}},"type":"binary-expr","locations":[{"start":{"line":52,"column":61},"end":{"line":54,"column":null}},{"start":{"line":54,"column":35},"end":{"line":54,"column":53}}]},"42":{"loc":{"start":{"line":54,"column":53},"end":{"line":56,"column":null}},"type":"cond-expr","locations":[{"start":{"line":56,"column":75},"end":{"line":56,"column":null}},{"start":{"line":56,"column":75},"end":{"line":56,"column":null}}]},"43":{"loc":{"start":{"line":54,"column":53},"end":{"line":56,"column":75}},"type":"binary-expr","locations":[{"start":{"line":54,"column":53},"end":{"line":56,"column":null}},{"start":{"line":56,"column":46},"end":{"line":56,"column":75}}]},"44":{"loc":{"start":{"line":56,"column":75},"end":{"line":57,"column":null}},"type":"cond-expr","locations":[{"start":{"line":57,"column":51},"end":{"line":57,"column":null}},{"start":{"line":57,"column":51},"end":{"line":57,"column":null}}]},"45":{"loc":{"start":{"line":56,"column":75},"end":{"line":57,"column":51}},"type":"binary-expr","locations":[{"start":{"line":56,"column":75},"end":{"line":57,"column":null}},{"start":{"line":57,"column":39},"end":{"line":57,"column":51}}]},"46":{"loc":{"start":{"line":57,"column":51},"end":{"line":58,"column":null}},"type":"cond-expr","locations":[{"start":{"line":58,"column":53},"end":{"line":58,"column":null}},{"start":{"line":58,"column":53},"end":{"line":58,"column":null}}]},"47":{"loc":{"start":{"line":57,"column":51},"end":{"line":58,"column":53}},"type":"binary-expr","locations":[{"start":{"line":57,"column":51},"end":{"line":58,"column":null}},{"start":{"line":58,"column":40},"end":{"line":58,"column":53}}]}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":3,"6":3,"7":3,"8":3,"9":3,"10":3,"11":3,"12":3,"13":3,"14":2,"15":2,"16":2,"17":2,"18":2,"19":2,"20":2,"21":2,"22":2,"23":2,"24":7,"25":7,"26":1,"27":1,"28":6,"29":6,"30":2,"31":2,"32":2,"33":2,"34":0,"35":0,"36":2,"37":2,"38":3,"39":3,"40":1,"41":1,"42":2,"43":2,"44":0,"45":0,"46":2,"47":2,"48":3,"49":3,"50":1,"51":1,"52":2,"53":2,"54":2,"55":2,"56":2,"57":0,"58":0,"59":2,"60":2,"61":2,"62":2,"63":2,"64":2,"65":0,"66":0,"67":2,"68":2,"69":0,"70":0,"71":2,"72":2,"73":2,"74":2,"75":0,"76":0,"77":2,"78":2,"79":2,"80":1,"81":1,"82":0,"83":0,"84":1,"85":1,"86":1,"87":2,"88":15,"89":15,"90":0,"91":0,"92":15,"93":0,"94":0,"95":15,"96":0,"97":0,"98":15,"99":15,"100":7,"101":8,"102":2,"103":6,"104":15,"105":1,"106":1,"107":14,"108":14,"109":14,"110":14,"111":11,"112":11,"113":11,"114":3,"115":3,"116":3,"117":3,"118":15,"119":15,"120":15,"121":15,"122":3,"123":3},"f":{"0":2,"1":7,"2":2,"3":3,"4":3,"5":2,"6":2,"7":15,"8":15},"b":{"0":[1,6],"1":[3,3],"2":[0,2],"3":[1,1],"4":[1,2],"5":[0,2],"6":[2,0],"7":[1,2],"8":[0,2],"9":[2,0],"10":[0,2],"11":[0,2],"12":[2,0],"13":[0,2],"14":[0,1],"15":[1,0],"16":[0,15],"17":[0,15],"18":[0,15],"19":[7,8],"20":[2,6],"21":[1,14],"22":[14,14],"23":[7,7],"24":[2,12],"25":[3,0],"26":[3,0],"27":[15,0],"28":[15,0],"29":[0,15],"30":[0,3],"31":[3,0],"32":[0,3],"33":[3,0],"34":[0,3],"35":[3,0],"36":[0,3],"37":[3,0],"38":[0,3],"39":[3,0],"40":[0,3],"41":[3,0],"42":[0,3],"43":[3,0],"44":[3,0],"45":[3,3],"46":[3,0],"47":[3,3]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/infrastructure/prisma-repositories/notification-log.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/infrastructure/prisma-repositories/notification-log.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":44}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":75}},"4":{"start":{"line":12,"column":7},"end":{"line":93,"column":null}},"5":{"start":{"line":13,"column":45},"end":{"line":13,"column":72}},"6":{"start":{"line":15,"column":33},"end":{"line":15,"column":41}},"7":{"start":{"line":22,"column":23},"end":{"line":30,"column":10}},"8":{"start":{"line":32,"column":8},"end":{"line":32,"column":70}},"9":{"start":{"line":40,"column":23},"end":{"line":48,"column":10}},"10":{"start":{"line":50,"column":8},"end":{"line":50,"column":70}},"11":{"start":{"line":61,"column":23},"end":{"line":68,"column":10}},"12":{"start":{"line":70,"column":8},"end":{"line":70,"column":70}},"13":{"start":{"line":74,"column":24},"end":{"line":76,"column":10}},"14":{"start":{"line":77,"column":8},"end":{"line":77,"column":55}},"15":{"start":{"line":81,"column":8},"end":{"line":84,"column":11}},"16":{"start":{"line":88,"column":8},"end":{"line":91,"column":11}},"17":{"start":{"line":12,"column":13},"end":{"line":12,"column":38}},"18":{"start":{"line":12,"column":13},"end":{"line":93,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":15,"column":4},"end":{"line":15,"column":33}},"loc":{"start":{"line":15,"column":54},"end":{"line":15,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":17,"column":4},"end":{"line":17,"column":9}},"loc":{"start":{"line":20,"column":31},"end":{"line":33,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":9}},"loc":{"start":{"line":38,"column":31},"end":{"line":51,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":53,"column":4},"end":{"line":53,"column":9}},"loc":{"start":{"line":55,"column":31},"end":{"line":71,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":73,"column":4},"end":{"line":73,"column":9}},"loc":{"start":{"line":73,"column":43},"end":{"line":78,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":80,"column":4},"end":{"line":80,"column":9}},"loc":{"start":{"line":80,"column":29},"end":{"line":85,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":87,"column":4},"end":{"line":87,"column":9}},"loc":{"start":{"line":87,"column":54},"end":{"line":92,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":32,"column":15},"end":{"line":32,"column":69}},"type":"cond-expr","locations":[{"start":{"line":32,"column":24},"end":{"line":32,"column":62}},{"start":{"line":32,"column":65},"end":{"line":32,"column":69}}]},"1":{"loc":{"start":{"line":50,"column":15},"end":{"line":50,"column":69}},"type":"cond-expr","locations":[{"start":{"line":50,"column":24},"end":{"line":50,"column":62}},{"start":{"line":50,"column":65},"end":{"line":50,"column":69}}]},"2":{"loc":{"start":{"line":70,"column":15},"end":{"line":70,"column":69}},"type":"cond-expr","locations":[{"start":{"line":70,"column":24},"end":{"line":70,"column":62}},{"start":{"line":70,"column":65},"end":{"line":70,"column":69}}]},"3":{"loc":{"start":{"line":15,"column":41},"end":{"line":15,"column":57}},"type":"cond-expr","locations":[{"start":{"line":15,"column":54},"end":{"line":15,"column":57}},{"start":{"line":15,"column":54},"end":{"line":15,"column":57}}]},"4":{"loc":{"start":{"line":15,"column":41},"end":{"line":15,"column":54}},"type":"binary-expr","locations":[{"start":{"line":15,"column":41},"end":{"line":15,"column":57}},{"start":{"line":15,"column":41},"end":{"line":15,"column":54}}]}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":3,"6":3,"7":12,"8":12,"9":5,"10":5,"11":13,"12":13,"13":29,"14":27,"15":13,"16":5,"17":3,"18":3},"f":{"0":3,"1":12,"2":5,"3":13,"4":29,"5":13,"6":5},"b":{"0":[4,8],"1":[2,3],"2":[6,7],"3":[3,0],"4":[3,3]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/position-hierarchy.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/position-hierarchy.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":73}},"1":{"start":{"line":5,"column":0},"end":{"line":5,"column":105}},"2":{"start":{"line":6,"column":0},"end":{"line":6,"column":53}},"3":{"start":{"line":9,"column":7},"end":{"line":67,"column":null}},"4":{"start":{"line":12,"column":25},"end":{"line":12,"column":67}},"5":{"start":{"line":13,"column":25},"end":{"line":13,"column":36}},"6":{"start":{"line":20,"column":8},"end":{"line":24,"column":9}},"7":{"start":{"line":21,"column":12},"end":{"line":23,"column":14}},"8":{"start":{"line":26,"column":8},"end":{"line":26,"column":57}},"9":{"start":{"line":27,"column":8},"end":{"line":27,"column":60}},"10":{"start":{"line":29,"column":8},"end":{"line":29,"column":78}},"11":{"start":{"line":36,"column":8},"end":{"line":36,"column":57}},"12":{"start":{"line":37,"column":8},"end":{"line":37,"column":60}},"13":{"start":{"line":38,"column":8},"end":{"line":38,"column":79}},"14":{"start":{"line":44,"column":8},"end":{"line":44,"column":57}},"15":{"start":{"line":46,"column":12},"end":{"line":46,"column":69}},"16":{"start":{"line":47,"column":8},"end":{"line":51,"column":10}},"17":{"start":{"line":49,"column":16},"end":{"line":49,"column":65}},"18":{"start":{"line":57,"column":8},"end":{"line":57,"column":60}},"19":{"start":{"line":58,"column":26},"end":{"line":59,"column":null}},"20":{"start":{"line":61,"column":8},"end":{"line":65,"column":10}},"21":{"start":{"line":63,"column":16},"end":{"line":63,"column":62}},"22":{"start":{"line":9,"column":13},"end":{"line":9,"column":37}},"23":{"start":{"line":9,"column":13},"end":{"line":67,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":10,"column":4},"end":{"line":10,"column":null}},"loc":{"start":{"line":13,"column":51},"end":{"line":14,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":9}},"loc":{"start":{"line":18,"column":37},"end":{"line":30,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":32,"column":4},"end":{"line":32,"column":9}},"loc":{"start":{"line":34,"column":37},"end":{"line":39,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":41,"column":4},"end":{"line":41,"column":9}},"loc":{"start":{"line":42,"column":34},"end":{"line":52,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":48,"column":26},"end":{"line":48,"column":27}},"loc":{"start":{"line":49,"column":16},"end":{"line":49,"column":65}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":54,"column":4},"end":{"line":54,"column":9}},"loc":{"start":{"line":55,"column":37},"end":{"line":66,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":62,"column":26},"end":{"line":62,"column":27}},"loc":{"start":{"line":63,"column":16},"end":{"line":63,"column":62}}}},"branchMap":{"0":{"loc":{"start":{"line":20,"column":8},"end":{"line":24,"column":9}},"type":"if","locations":[{"start":{"line":20,"column":8},"end":{"line":24,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":13,"column":36},"end":{"line":13,"column":null}},"type":"cond-expr","locations":[{"start":{"line":13,"column":51},"end":{"line":13,"column":null}},{"start":{"line":13,"column":51},"end":{"line":13,"column":null}}]},"2":{"loc":{"start":{"line":13,"column":36},"end":{"line":13,"column":51}},"type":"binary-expr","locations":[{"start":{"line":13,"column":36},"end":{"line":13,"column":null}},{"start":{"line":13,"column":36},"end":{"line":13,"column":51}}]}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":11,"7":1,"8":10,"9":9,"10":8,"11":2,"12":2,"13":2,"14":2,"15":2,"16":2,"17":2,"18":2,"19":2,"20":2,"21":2,"22":6,"23":6},"f":{"0":6,"1":11,"2":2,"3":2,"4":2,"5":2,"6":2},"b":{"0":[1,10],"1":[6,0],"2":[6,6]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/position.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/position.service.ts","statementMap":{"0":{"start":{"line":6,"column":0},"end":{"line":6,"column":null}},"1":{"start":{"line":12,"column":0},"end":{"line":12,"column":null}},"2":{"start":{"line":16,"column":0},"end":{"line":16,"column":62}},"3":{"start":{"line":18,"column":0},"end":{"line":18,"column":85}},"4":{"start":{"line":21,"column":7},"end":{"line":84,"column":null}},"5":{"start":{"line":24,"column":25},"end":{"line":24,"column":58}},"6":{"start":{"line":26,"column":25},"end":{"line":26,"column":54}},"7":{"start":{"line":30,"column":25},"end":{"line":33,"column":10}},"8":{"start":{"line":34,"column":8},"end":{"line":34,"column":47}},"9":{"start":{"line":38,"column":8},"end":{"line":38,"column":44}},"10":{"start":{"line":42,"column":22},"end":{"line":42,"column":55}},"11":{"start":{"line":43,"column":8},"end":{"line":43,"column":70}},"12":{"start":{"line":43,"column":20},"end":{"line":43,"column":70}},"13":{"start":{"line":44,"column":8},"end":{"line":44,"column":21}},"14":{"start":{"line":51,"column":8},"end":{"line":51,"column":31}},"15":{"start":{"line":52,"column":47},"end":{"line":57,"column":10}},"16":{"start":{"line":58,"column":8},"end":{"line":58,"column":54}},"17":{"start":{"line":62,"column":8},"end":{"line":62,"column":31}},"18":{"start":{"line":63,"column":28},"end":{"line":63,"column":58}},"19":{"start":{"line":64,"column":8},"end":{"line":72,"column":9}},"20":{"start":{"line":65,"column":12},"end":{"line":71,"column":14}},"21":{"start":{"line":75,"column":8},"end":{"line":75,"column":44}},"22":{"start":{"line":79,"column":8},"end":{"line":79,"column":39}},"23":{"start":{"line":81,"column":12},"end":{"line":81,"column":77}},"24":{"start":{"line":82,"column":8},"end":{"line":82,"column":52}},"25":{"start":{"line":82,"column":36},"end":{"line":82,"column":50}},"26":{"start":{"line":21,"column":13},"end":{"line":21,"column":28}},"27":{"start":{"line":21,"column":13},"end":{"line":84,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":22,"column":4},"end":{"line":22,"column":null}},"loc":{"start":{"line":26,"column":94},"end":{"line":27,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":29,"column":4},"end":{"line":29,"column":9}},"loc":{"start":{"line":29,"column":47},"end":{"line":35,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":37,"column":4},"end":{"line":37,"column":9}},"loc":{"start":{"line":37,"column":43},"end":{"line":39,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":41,"column":4},"end":{"line":41,"column":9}},"loc":{"start":{"line":41,"column":28},"end":{"line":45,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":47,"column":4},"end":{"line":47,"column":9}},"loc":{"start":{"line":49,"column":36},"end":{"line":59,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":61,"column":4},"end":{"line":61,"column":9}},"loc":{"start":{"line":61,"column":27},"end":{"line":76,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":78,"column":4},"end":{"line":78,"column":9}},"loc":{"start":{"line":78,"column":44},"end":{"line":83,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":82,"column":29},"end":{"line":82,"column":30}},"loc":{"start":{"line":82,"column":36},"end":{"line":82,"column":50}}}},"branchMap":{"0":{"loc":{"start":{"line":32,"column":25},"end":{"line":32,"column":52}},"type":"binary-expr","locations":[{"start":{"line":32,"column":25},"end":{"line":32,"column":44}},{"start":{"line":32,"column":48},"end":{"line":32,"column":52}}]},"1":{"loc":{"start":{"line":43,"column":8},"end":{"line":43,"column":70}},"type":"if","locations":[{"start":{"line":43,"column":8},"end":{"line":43,"column":70}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":54,"column":16},"end":{"line":56,"column":20}},"type":"cond-expr","locations":[{"start":{"line":55,"column":18},"end":{"line":55,"column":52}},{"start":{"line":56,"column":18},"end":{"line":56,"column":20}}]},"3":{"loc":{"start":{"line":64,"column":8},"end":{"line":72,"column":9}},"type":"if","locations":[{"start":{"line":64,"column":8},"end":{"line":72,"column":9}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":26,"column":54},"end":{"line":26,"column":null}},"type":"cond-expr","locations":[{"start":{"line":26,"column":94},"end":{"line":26,"column":null}},{"start":{"line":26,"column":94},"end":{"line":26,"column":null}}]},"5":{"loc":{"start":{"line":26,"column":54},"end":{"line":26,"column":94}},"type":"binary-expr","locations":[{"start":{"line":26,"column":54},"end":{"line":26,"column":null}},{"start":{"line":26,"column":54},"end":{"line":26,"column":94}}]}},"s":{"0":50,"1":50,"2":50,"3":50,"4":50,"5":50,"6":50,"7":108,"8":108,"9":2,"10":53,"11":53,"12":7,"13":46,"14":2,"15":1,"16":1,"17":3,"18":2,"19":2,"20":1,"21":1,"22":4,"23":4,"24":4,"25":3,"26":50,"27":50},"f":{"0":50,"1":108,"2":2,"3":53,"4":2,"5":3,"6":4,"7":3},"b":{"0":[108,106],"1":[7,46],"2":[1,0],"3":[1,1],"4":[0,50],"5":[50,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/team.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/team.service.ts","statementMap":{"0":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"1":{"start":{"line":13,"column":0},"end":{"line":13,"column":103}},"2":{"start":{"line":15,"column":0},"end":{"line":15,"column":54}},"3":{"start":{"line":17,"column":0},"end":{"line":17,"column":77}},"4":{"start":{"line":20,"column":7},"end":{"line":130,"column":null}},"5":{"start":{"line":23,"column":25},"end":{"line":23,"column":50}},"6":{"start":{"line":24,"column":25},"end":{"line":24,"column":40}},"7":{"start":{"line":28,"column":8},"end":{"line":30,"column":9}},"8":{"start":{"line":29,"column":12},"end":{"line":29,"column":61}},"9":{"start":{"line":32,"column":21},"end":{"line":36,"column":10}},"10":{"start":{"line":37,"column":8},"end":{"line":37,"column":39}},"11":{"start":{"line":41,"column":8},"end":{"line":41,"column":40}},"12":{"start":{"line":45,"column":21},"end":{"line":45,"column":50}},"13":{"start":{"line":46,"column":8},"end":{"line":46,"column":65}},"14":{"start":{"line":46,"column":19},"end":{"line":46,"column":65}},"15":{"start":{"line":47,"column":8},"end":{"line":47,"column":20}},"16":{"start":{"line":51,"column":8},"end":{"line":51,"column":31}},"17":{"start":{"line":53,"column":8},"end":{"line":55,"column":9}},"18":{"start":{"line":54,"column":12},"end":{"line":54,"column":59}},"19":{"start":{"line":57,"column":43},"end":{"line":62,"column":10}},"20":{"start":{"line":64,"column":8},"end":{"line":64,"column":50}},"21":{"start":{"line":68,"column":8},"end":{"line":68,"column":31}},"22":{"start":{"line":69,"column":24},"end":{"line":69,"column":50}},"23":{"start":{"line":70,"column":8},"end":{"line":78,"column":9}},"24":{"start":{"line":71,"column":12},"end":{"line":77,"column":14}},"25":{"start":{"line":79,"column":8},"end":{"line":79,"column":40}},"26":{"start":{"line":87,"column":8},"end":{"line":87,"column":35}},"27":{"start":{"line":88,"column":21},"end":{"line":88,"column":71}},"28":{"start":{"line":90,"column":27},"end":{"line":93,"column":null}},"29":{"start":{"line":95,"column":8},"end":{"line":97,"column":9}},"30":{"start":{"line":96,"column":12},"end":{"line":96,"column":45}},"31":{"start":{"line":98,"column":8},"end":{"line":98,"column":26}},"32":{"start":{"line":102,"column":8},"end":{"line":102,"column":35}},"33":{"start":{"line":103,"column":8},"end":{"line":103,"column":56}},"34":{"start":{"line":110,"column":8},"end":{"line":110,"column":35}},"35":{"start":{"line":111,"column":28},"end":{"line":111,"column":64}},"36":{"start":{"line":113,"column":8},"end":{"line":113,"column":49}},"37":{"start":{"line":113,"column":30},"end":{"line":113,"column":49}},"38":{"start":{"line":115,"column":26},"end":{"line":125,"column":null}},"39":{"start":{"line":117,"column":16},"end":{"line":124,"column":17}},"40":{"start":{"line":118,"column":33},"end":{"line":119,"column":null}},"41":{"start":{"line":121,"column":20},"end":{"line":121,"column":53}},"42":{"start":{"line":123,"column":20},"end":{"line":123,"column":38}},"43":{"start":{"line":128,"column":8},"end":{"line":128,"column":25}},"44":{"start":{"line":20,"column":13},"end":{"line":20,"column":24}},"45":{"start":{"line":20,"column":13},"end":{"line":130,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"loc":{"start":{"line":24,"column":59},"end":{"line":25,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":27,"column":4},"end":{"line":27,"column":9}},"loc":{"start":{"line":27,"column":43},"end":{"line":38,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":40,"column":4},"end":{"line":40,"column":9}},"loc":{"start":{"line":40,"column":39},"end":{"line":42,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":44,"column":4},"end":{"line":44,"column":9}},"loc":{"start":{"line":44,"column":28},"end":{"line":48,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":50,"column":4},"end":{"line":50,"column":9}},"loc":{"start":{"line":50,"column":53},"end":{"line":65,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":67,"column":4},"end":{"line":67,"column":9}},"loc":{"start":{"line":67,"column":27},"end":{"line":80,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":82,"column":4},"end":{"line":82,"column":9}},"loc":{"start":{"line":85,"column":37},"end":{"line":99,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":101,"column":4},"end":{"line":101,"column":9}},"loc":{"start":{"line":101,"column":55},"end":{"line":104,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":106,"column":4},"end":{"line":106,"column":9}},"loc":{"start":{"line":108,"column":38},"end":{"line":129,"column":5}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":116,"column":28},"end":{"line":116,"column":33}},"loc":{"start":{"line":116,"column":49},"end":{"line":125,"column":13}}}},"branchMap":{"0":{"loc":{"start":{"line":28,"column":8},"end":{"line":30,"column":9}},"type":"if","locations":[{"start":{"line":28,"column":8},"end":{"line":30,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":28,"column":12},"end":{"line":28,"column":67}},"type":"binary-expr","locations":[{"start":{"line":28,"column":12},"end":{"line":28,"column":40}},{"start":{"line":28,"column":44},"end":{"line":28,"column":67}}]},"2":{"loc":{"start":{"line":34,"column":25},"end":{"line":34,"column":52}},"type":"binary-expr","locations":[{"start":{"line":34,"column":25},"end":{"line":34,"column":44}},{"start":{"line":34,"column":48},"end":{"line":34,"column":52}}]},"3":{"loc":{"start":{"line":35,"column":20},"end":{"line":35,"column":42}},"type":"binary-expr","locations":[{"start":{"line":35,"column":20},"end":{"line":35,"column":34}},{"start":{"line":35,"column":38},"end":{"line":35,"column":42}}]},"4":{"loc":{"start":{"line":46,"column":8},"end":{"line":46,"column":65}},"type":"if","locations":[{"start":{"line":46,"column":8},"end":{"line":46,"column":65}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":53,"column":8},"end":{"line":55,"column":9}},"type":"if","locations":[{"start":{"line":53,"column":8},"end":{"line":55,"column":9}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":53,"column":12},"end":{"line":53,"column":63}},"type":"binary-expr","locations":[{"start":{"line":53,"column":12},"end":{"line":53,"column":38}},{"start":{"line":53,"column":42},"end":{"line":53,"column":63}}]},"7":{"loc":{"start":{"line":59,"column":16},"end":{"line":61,"column":20}},"type":"cond-expr","locations":[{"start":{"line":60,"column":18},"end":{"line":60,"column":52}},{"start":{"line":61,"column":18},"end":{"line":61,"column":20}}]},"8":{"loc":{"start":{"line":70,"column":8},"end":{"line":78,"column":9}},"type":"if","locations":[{"start":{"line":70,"column":8},"end":{"line":78,"column":9}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":95,"column":8},"end":{"line":97,"column":9}},"type":"if","locations":[{"start":{"line":95,"column":8},"end":{"line":97,"column":9}},{"start":{},"end":{}}]},"10":{"loc":{"start":{"line":113,"column":8},"end":{"line":113,"column":49}},"type":"if","locations":[{"start":{"line":113,"column":8},"end":{"line":113,"column":49}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":24,"column":40},"end":{"line":24,"column":null}},"type":"cond-expr","locations":[{"start":{"line":24,"column":59},"end":{"line":24,"column":null}},{"start":{"line":24,"column":59},"end":{"line":24,"column":null}}]},"12":{"loc":{"start":{"line":24,"column":40},"end":{"line":24,"column":59}},"type":"binary-expr","locations":[{"start":{"line":24,"column":40},"end":{"line":24,"column":null}},{"start":{"line":24,"column":40},"end":{"line":24,"column":59}}]}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":20,"8":2,"9":19,"10":19,"11":2,"12":23,"13":23,"14":4,"15":19,"16":3,"17":2,"18":2,"19":1,"20":1,"21":3,"22":2,"23":2,"24":1,"25":1,"26":10,"27":9,"28":8,"29":8,"30":1,"31":7,"32":1,"33":1,"34":4,"35":4,"36":4,"37":3,"38":1,"39":2,"40":2,"41":2,"42":0,"43":1,"44":6,"45":6},"f":{"0":6,"1":20,"2":2,"3":23,"4":3,"5":3,"6":10,"7":1,"8":4,"9":2},"b":{"0":[2,18],"1":[20,2],"2":[19,19],"3":[19,18],"4":[4,19],"5":[2,0],"6":[2,2],"7":[1,0],"8":[1,1],"9":[1,7],"10":[3,1],"11":[6,0],"12":[6,6]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/infrastructure/prisma-repositories/position-hierarchy.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/infrastructure/prisma-repositories/position-hierarchy.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":41}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":44}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":60}},"3":{"start":{"line":6,"column":0},"end":{"line":6,"column":79}},"4":{"start":{"line":9,"column":7},"end":{"line":70,"column":null}},"5":{"start":{"line":10,"column":33},"end":{"line":10,"column":41}},"6":{"start":{"line":16,"column":8},"end":{"line":39,"column":9}},"7":{"start":{"line":17,"column":28},"end":{"line":19,"column":14}},"8":{"start":{"line":20,"column":12},"end":{"line":20,"column":61}},"9":{"start":{"line":22,"column":12},"end":{"line":37,"column":13}},"10":{"start":{"line":26,"column":33},"end":{"line":34,"column":null}},"11":{"start":{"line":36,"column":16},"end":{"line":36,"column":80}},"12":{"start":{"line":36,"column":30},"end":{"line":36,"column":80}},"13":{"start":{"line":38,"column":12},"end":{"line":38,"column":24}},"14":{"start":{"line":46,"column":8},"end":{"line":48,"column":11}},"15":{"start":{"line":54,"column":26},"end":{"line":57,"column":10}},"16":{"start":{"line":58,"column":8},"end":{"line":58,"column":63}},"17":{"start":{"line":64,"column":26},"end":{"line":67,"column":10}},"18":{"start":{"line":68,"column":8},"end":{"line":68,"column":63}},"19":{"start":{"line":9,"column":13},"end":{"line":9,"column":40}},"20":{"start":{"line":9,"column":13},"end":{"line":70,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":10,"column":4},"end":{"line":10,"column":33}},"loc":{"start":{"line":10,"column":54},"end":{"line":10,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":9}},"loc":{"start":{"line":14,"column":37},"end":{"line":40,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":42,"column":4},"end":{"line":42,"column":9}},"loc":{"start":{"line":44,"column":37},"end":{"line":49,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":51,"column":4},"end":{"line":51,"column":9}},"loc":{"start":{"line":52,"column":34},"end":{"line":59,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":61,"column":4},"end":{"line":61,"column":9}},"loc":{"start":{"line":62,"column":37},"end":{"line":69,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":22,"column":12},"end":{"line":37,"column":13}},"type":"if","locations":[{"start":{"line":22,"column":12},"end":{"line":37,"column":13}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":23,"column":16},"end":{"line":24,"column":38}},"type":"binary-expr","locations":[{"start":{"line":23,"column":16},"end":{"line":23,"column":69}},{"start":{"line":24,"column":16},"end":{"line":24,"column":38}}]},"2":{"loc":{"start":{"line":36,"column":16},"end":{"line":36,"column":80}},"type":"if","locations":[{"start":{"line":36,"column":16},"end":{"line":36,"column":80}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":10,"column":41},"end":{"line":10,"column":57}},"type":"cond-expr","locations":[{"start":{"line":10,"column":54},"end":{"line":10,"column":57}},{"start":{"line":10,"column":54},"end":{"line":10,"column":57}}]},"4":{"loc":{"start":{"line":10,"column":41},"end":{"line":10,"column":54}},"type":"binary-expr","locations":[{"start":{"line":10,"column":41},"end":{"line":10,"column":57}},{"start":{"line":10,"column":41},"end":{"line":10,"column":54}}]}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":14,"7":14,"8":12,"9":2,"10":2,"11":2,"12":2,"13":0,"14":4,"15":4,"16":4,"17":4,"18":4,"19":6,"20":6},"f":{"0":6,"1":14,"2":4,"3":4,"4":4},"b":{"0":[2,0],"1":[2,2],"2":[2,0],"3":[6,0],"4":[6,6]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/infrastructure/prisma-repositories/position.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/infrastructure/prisma-repositories/position.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"2":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"3":{"start":{"line":12,"column":0},"end":{"line":12,"column":60}},"4":{"start":{"line":15,"column":7},"end":{"line":94,"column":null}},"5":{"start":{"line":16,"column":33},"end":{"line":16,"column":41}},"6":{"start":{"line":19,"column":24},"end":{"line":21,"column":10}},"7":{"start":{"line":22,"column":8},"end":{"line":22,"column":48}},"8":{"start":{"line":26,"column":22},"end":{"line":26,"column":78}},"9":{"start":{"line":27,"column":8},"end":{"line":27,"column":61}},"10":{"start":{"line":31,"column":22},"end":{"line":31,"column":44}},"11":{"start":{"line":32,"column":24},"end":{"line":32,"column":46}},"12":{"start":{"line":34,"column":22},"end":{"line":37,"column":10}},"13":{"start":{"line":39,"column":8},"end":{"line":39,"column":50}},"14":{"start":{"line":46,"column":24},"end":{"line":49,"column":10}},"15":{"start":{"line":50,"column":8},"end":{"line":50,"column":48}},"16":{"start":{"line":54,"column":8},"end":{"line":54,"column":61}},"17":{"start":{"line":58,"column":47},"end":{"line":58,"column":52}},"18":{"start":{"line":59,"column":8},"end":{"line":84,"column":10}},"19":{"start":{"line":90,"column":22},"end":{"line":90,"column":58}},"20":{"start":{"line":91,"column":26},"end":{"line":91,"column":66}},"21":{"start":{"line":92,"column":8},"end":{"line":92,"column":74}},"22":{"start":{"line":15,"column":13},"end":{"line":15,"column":31}},"23":{"start":{"line":15,"column":13},"end":{"line":94,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":33}},"loc":{"start":{"line":16,"column":54},"end":{"line":16,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":18,"column":41},"end":{"line":23,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":25,"column":4},"end":{"line":25,"column":9}},"loc":{"start":{"line":25,"column":29},"end":{"line":28,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":30,"column":4},"end":{"line":30,"column":9}},"loc":{"start":{"line":30,"column":43},"end":{"line":40,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":42,"column":4},"end":{"line":42,"column":9}},"loc":{"start":{"line":44,"column":36},"end":{"line":51,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":53,"column":4},"end":{"line":53,"column":9}},"loc":{"start":{"line":53,"column":31},"end":{"line":55,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":57,"column":12},"end":{"line":57,"column":22}},"loc":{"start":{"line":57,"column":49},"end":{"line":85,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":87,"column":12},"end":{"line":87,"column":22}},"loc":{"start":{"line":88,"column":34},"end":{"line":93,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":27,"column":15},"end":{"line":27,"column":60}},"type":"cond-expr","locations":[{"start":{"line":27,"column":23},"end":{"line":27,"column":53}},{"start":{"line":27,"column":56},"end":{"line":27,"column":60}}]},"1":{"loc":{"start":{"line":60,"column":16},"end":{"line":72,"column":20}},"type":"cond-expr","locations":[{"start":{"line":61,"column":18},"end":{"line":71,"column":null}},{"start":{"line":72,"column":18},"end":{"line":72,"column":20}}]},"2":{"loc":{"start":{"line":73,"column":16},"end":{"line":75,"column":20}},"type":"cond-expr","locations":[{"start":{"line":74,"column":18},"end":{"line":74,"column":69}},{"start":{"line":75,"column":18},"end":{"line":75,"column":20}}]},"3":{"loc":{"start":{"line":76,"column":16},"end":{"line":83,"column":20}},"type":"cond-expr","locations":[{"start":{"line":77,"column":18},"end":{"line":82,"column":null}},{"start":{"line":83,"column":18},"end":{"line":83,"column":20}}]},"4":{"loc":{"start":{"line":90,"column":22},"end":{"line":90,"column":58}},"type":"binary-expr","locations":[{"start":{"line":90,"column":22},"end":{"line":90,"column":34}},{"start":{"line":90,"column":38},"end":{"line":90,"column":58}}]},"5":{"loc":{"start":{"line":91,"column":26},"end":{"line":91,"column":66}},"type":"binary-expr","locations":[{"start":{"line":91,"column":26},"end":{"line":91,"column":45}},{"start":{"line":91,"column":49},"end":{"line":91,"column":66}}]},"6":{"loc":{"start":{"line":16,"column":41},"end":{"line":16,"column":57}},"type":"cond-expr","locations":[{"start":{"line":16,"column":54},"end":{"line":16,"column":57}},{"start":{"line":16,"column":54},"end":{"line":16,"column":57}}]},"7":{"loc":{"start":{"line":16,"column":41},"end":{"line":16,"column":54}},"type":"binary-expr","locations":[{"start":{"line":16,"column":41},"end":{"line":16,"column":57}},{"start":{"line":16,"column":41},"end":{"line":16,"column":54}}]}},"s":{"0":53,"1":53,"2":53,"3":53,"4":53,"5":53,"6":365,"7":365,"8":55,"9":55,"10":7,"11":7,"12":7,"13":7,"14":2,"15":2,"16":2,"17":7,"18":7,"19":7,"20":7,"21":7,"22":53,"23":53},"f":{"0":53,"1":365,"2":55,"3":7,"4":2,"5":2,"6":7,"7":7},"b":{"0":[47,8],"1":[0,7],"2":[2,5],"3":[0,7],"4":[7,6],"5":[7,6],"6":[53,0],"7":[53,53]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/infrastructure/prisma-repositories/team.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/infrastructure/prisma-repositories/team.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":41}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":44}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":60}},"4":{"start":{"line":13,"column":0},"end":{"line":13,"column":73}},"5":{"start":{"line":14,"column":0},"end":{"line":14,"column":52}},"6":{"start":{"line":17,"column":7},"end":{"line":142,"column":null}},"7":{"start":{"line":18,"column":33},"end":{"line":18,"column":41}},"8":{"start":{"line":21,"column":24},"end":{"line":23,"column":10}},"9":{"start":{"line":24,"column":8},"end":{"line":24,"column":44}},"10":{"start":{"line":28,"column":22},"end":{"line":28,"column":74}},"11":{"start":{"line":29,"column":8},"end":{"line":29,"column":57}},"12":{"start":{"line":33,"column":22},"end":{"line":33,"column":44}},"13":{"start":{"line":34,"column":24},"end":{"line":34,"column":46}},"14":{"start":{"line":36,"column":22},"end":{"line":36,"column":73}},"15":{"start":{"line":38,"column":8},"end":{"line":38,"column":46}},"16":{"start":{"line":45,"column":24},"end":{"line":48,"column":10}},"17":{"start":{"line":49,"column":8},"end":{"line":49,"column":44}},"18":{"start":{"line":53,"column":8},"end":{"line":53,"column":57}},"19":{"start":{"line":61,"column":8},"end":{"line":87,"column":9}},"20":{"start":{"line":62,"column":28},"end":{"line":68,"column":14}},"21":{"start":{"line":69,"column":12},"end":{"line":69,"column":58}},"22":{"start":{"line":71,"column":12},"end":{"line":85,"column":13}},"23":{"start":{"line":75,"column":33},"end":{"line":83,"column":18}},"24":{"start":{"line":84,"column":16},"end":{"line":84,"column":77}},"25":{"start":{"line":84,"column":30},"end":{"line":84,"column":77}},"26":{"start":{"line":86,"column":12},"end":{"line":86,"column":24}},"27":{"start":{"line":91,"column":8},"end":{"line":93,"column":11}},"28":{"start":{"line":97,"column":28},"end":{"line":100,"column":10}},"29":{"start":{"line":101,"column":8},"end":{"line":101,"column":62}},"30":{"start":{"line":105,"column":55},"end":{"line":105,"column":60}},"31":{"start":{"line":106,"column":8},"end":{"line":132,"column":10}},"32":{"start":{"line":138,"column":22},"end":{"line":138,"column":54}},"33":{"start":{"line":139,"column":26},"end":{"line":139,"column":66}},"34":{"start":{"line":140,"column":8},"end":{"line":140,"column":74}},"35":{"start":{"line":17,"column":13},"end":{"line":17,"column":27}},"36":{"start":{"line":17,"column":13},"end":{"line":142,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":33}},"loc":{"start":{"line":18,"column":54},"end":{"line":18,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":20,"column":4},"end":{"line":20,"column":9}},"loc":{"start":{"line":20,"column":33},"end":{"line":25,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":27,"column":4},"end":{"line":27,"column":9}},"loc":{"start":{"line":27,"column":29},"end":{"line":30,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":32,"column":4},"end":{"line":32,"column":9}},"loc":{"start":{"line":32,"column":39},"end":{"line":39,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":41,"column":4},"end":{"line":41,"column":9}},"loc":{"start":{"line":43,"column":32},"end":{"line":50,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":52,"column":4},"end":{"line":52,"column":9}},"loc":{"start":{"line":52,"column":31},"end":{"line":54,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":56,"column":4},"end":{"line":56,"column":9}},"loc":{"start":{"line":59,"column":34},"end":{"line":88,"column":5}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":90,"column":4},"end":{"line":90,"column":9}},"loc":{"start":{"line":90,"column":55},"end":{"line":94,"column":5}}},"8":{"name":"(anonymous_10)","decl":{"start":{"line":96,"column":4},"end":{"line":96,"column":9}},"loc":{"start":{"line":96,"column":36},"end":{"line":102,"column":5}}},"9":{"name":"(anonymous_11)","decl":{"start":{"line":104,"column":12},"end":{"line":104,"column":22}},"loc":{"start":{"line":104,"column":45},"end":{"line":133,"column":5}}},"10":{"name":"(anonymous_12)","decl":{"start":{"line":135,"column":12},"end":{"line":135,"column":22}},"loc":{"start":{"line":136,"column":30},"end":{"line":141,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":29,"column":15},"end":{"line":29,"column":56}},"type":"cond-expr","locations":[{"start":{"line":29,"column":23},"end":{"line":29,"column":49}},{"start":{"line":29,"column":52},"end":{"line":29,"column":56}}]},"1":{"loc":{"start":{"line":66,"column":31},"end":{"line":66,"column":49}},"type":"binary-expr","locations":[{"start":{"line":66,"column":31},"end":{"line":66,"column":40}},{"start":{"line":66,"column":44},"end":{"line":66,"column":49}}]},"2":{"loc":{"start":{"line":71,"column":12},"end":{"line":85,"column":13}},"type":"if","locations":[{"start":{"line":71,"column":12},"end":{"line":85,"column":13}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":72,"column":16},"end":{"line":73,"column":38}},"type":"binary-expr","locations":[{"start":{"line":72,"column":16},"end":{"line":72,"column":69}},{"start":{"line":73,"column":16},"end":{"line":73,"column":38}}]},"4":{"loc":{"start":{"line":80,"column":39},"end":{"line":80,"column":57}},"type":"binary-expr","locations":[{"start":{"line":80,"column":39},"end":{"line":80,"column":48}},{"start":{"line":80,"column":52},"end":{"line":80,"column":57}}]},"5":{"loc":{"start":{"line":84,"column":16},"end":{"line":84,"column":77}},"type":"if","locations":[{"start":{"line":84,"column":16},"end":{"line":84,"column":77}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":107,"column":16},"end":{"line":109,"column":20}},"type":"cond-expr","locations":[{"start":{"line":108,"column":18},"end":{"line":108,"column":69}},{"start":{"line":109,"column":18},"end":{"line":109,"column":20}}]},"7":{"loc":{"start":{"line":110,"column":16},"end":{"line":117,"column":20}},"type":"cond-expr","locations":[{"start":{"line":111,"column":18},"end":{"line":116,"column":null}},{"start":{"line":117,"column":18},"end":{"line":117,"column":20}}]},"8":{"loc":{"start":{"line":118,"column":16},"end":{"line":118,"column":54}},"type":"cond-expr","locations":[{"start":{"line":118,"column":39},"end":{"line":118,"column":49}},{"start":{"line":118,"column":52},"end":{"line":118,"column":54}}]},"9":{"loc":{"start":{"line":119,"column":16},"end":{"line":131,"column":20}},"type":"cond-expr","locations":[{"start":{"line":120,"column":18},"end":{"line":130,"column":null}},{"start":{"line":131,"column":18},"end":{"line":131,"column":20}}]},"10":{"loc":{"start":{"line":138,"column":22},"end":{"line":138,"column":54}},"type":"binary-expr","locations":[{"start":{"line":138,"column":22},"end":{"line":138,"column":34}},{"start":{"line":138,"column":38},"end":{"line":138,"column":54}}]},"11":{"loc":{"start":{"line":139,"column":26},"end":{"line":139,"column":66}},"type":"binary-expr","locations":[{"start":{"line":139,"column":26},"end":{"line":139,"column":45}},{"start":{"line":139,"column":49},"end":{"line":139,"column":66}}]},"12":{"loc":{"start":{"line":18,"column":41},"end":{"line":18,"column":57}},"type":"cond-expr","locations":[{"start":{"line":18,"column":54},"end":{"line":18,"column":57}},{"start":{"line":18,"column":54},"end":{"line":18,"column":57}}]},"13":{"loc":{"start":{"line":18,"column":41},"end":{"line":18,"column":54}},"type":"binary-expr","locations":[{"start":{"line":18,"column":41},"end":{"line":18,"column":57}},{"start":{"line":18,"column":41},"end":{"line":18,"column":54}}]}},"s":{"0":41,"1":41,"2":41,"3":41,"4":41,"5":41,"6":41,"7":41,"8":40,"9":40,"10":25,"11":25,"12":5,"13":5,"14":5,"15":5,"16":2,"17":2,"18":2,"19":15,"20":15,"21":14,"22":1,"23":1,"24":1,"25":1,"26":0,"27":2,"28":5,"29":5,"30":5,"31":5,"32":5,"33":5,"34":5,"35":41,"36":41},"f":{"0":41,"1":40,"2":25,"3":5,"4":2,"5":2,"6":15,"7":2,"8":5,"9":5,"10":5},"b":{"0":[20,5],"1":[15,1],"2":[1,0],"3":[1,1],"4":[1,0],"5":[1,0],"6":[1,4],"7":[0,5],"8":[1,4],"9":[0,5],"10":[5,4],"11":[5,4],"12":[41,0],"13":[41,41]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/listeners/cycle-stage.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/listeners/cycle-stage.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":50}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":48}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":114}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":111}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":null}},"6":{"start":{"line":10,"column":0},"end":{"line":10,"column":87}},"7":{"start":{"line":18,"column":31},"end":{"line":86,"column":null}},"8":{"start":{"line":19,"column":30},"end":{"line":19,"column":65}},"9":{"start":{"line":22,"column":25},"end":{"line":22,"column":52}},"10":{"start":{"line":23,"column":25},"end":{"line":23,"column":39}},"11":{"start":{"line":25,"column":25},"end":{"line":25,"column":34}},"12":{"start":{"line":39,"column":8},"end":{"line":41,"column":9}},"13":{"start":{"line":40,"column":12},"end":{"line":40,"column":19}},"14":{"start":{"line":43,"column":8},"end":{"line":45,"column":10}},"15":{"start":{"line":47,"column":8},"end":{"line":54,"column":18}},"16":{"start":{"line":48,"column":28},"end":{"line":48,"column":75}},"17":{"start":{"line":49,"column":12},"end":{"line":53,"column":13}},"18":{"start":{"line":50,"column":16},"end":{"line":52,"column":18}},"19":{"start":{"line":56,"column":8},"end":{"line":58,"column":10}},"20":{"start":{"line":60,"column":8},"end":{"line":84,"column":9}},"21":{"start":{"line":61,"column":12},"end":{"line":64,"column":14}},"22":{"start":{"line":67,"column":16},"end":{"line":68,"column":null}},"23":{"start":{"line":71,"column":12},"end":{"line":73,"column":14}},"24":{"start":{"line":75,"column":12},"end":{"line":78,"column":14}},"25":{"start":{"line":80,"column":12},"end":{"line":83,"column":14}},"26":{"start":{"line":18,"column":13},"end":{"line":18,"column":31}},"27":{"start":{"line":36,"column":10},"end":{"line":85,"column":null}},"28":{"start":{"line":18,"column":13},"end":{"line":86,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"loc":{"start":{"line":25,"column":54},"end":{"line":26,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":36,"column":10},"end":{"line":36,"column":35}},"loc":{"start":{"line":37,"column":39},"end":{"line":85,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":47,"column":19},"end":{"line":47,"column":24}},"loc":{"start":{"line":47,"column":30},"end":{"line":54,"column":9}}}},"branchMap":{"0":{"loc":{"start":{"line":39,"column":8},"end":{"line":41,"column":9}},"type":"if","locations":[{"start":{"line":39,"column":8},"end":{"line":41,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":49,"column":12},"end":{"line":53,"column":13}},"type":"if","locations":[{"start":{"line":49,"column":12},"end":{"line":53,"column":13}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":37,"column":15},"end":{"line":37,"column":null}},"type":"cond-expr","locations":[{"start":{"line":37,"column":39},"end":{"line":37,"column":null}},{"start":{"line":37,"column":39},"end":{"line":37,"column":null}}]},"3":{"loc":{"start":{"line":37,"column":15},"end":{"line":37,"column":39}},"type":"binary-expr","locations":[{"start":{"line":37,"column":15},"end":{"line":37,"column":null}},{"start":{"line":37,"column":15},"end":{"line":37,"column":39}}]},"4":{"loc":{"start":{"line":38,"column":7},"end":{"line":38,"column":null}},"type":"cond-expr","locations":[{"start":{"line":38,"column":14},"end":{"line":38,"column":null}},{"start":{"line":38,"column":14},"end":{"line":38,"column":null}}]},"5":{"loc":{"start":{"line":38,"column":7},"end":{"line":38,"column":14}},"type":"binary-expr","locations":[{"start":{"line":38,"column":7},"end":{"line":38,"column":null}},{"start":{"line":38,"column":7},"end":{"line":38,"column":14}}]},"6":{"loc":{"start":{"line":22,"column":52},"end":{"line":22,"column":null}},"type":"cond-expr","locations":[{"start":{"line":22,"column":77},"end":{"line":22,"column":null}},{"start":{"line":22,"column":77},"end":{"line":22,"column":null}}]},"7":{"loc":{"start":{"line":22,"column":52},"end":{"line":22,"column":77}},"type":"binary-expr","locations":[{"start":{"line":22,"column":52},"end":{"line":22,"column":null}},{"start":{"line":22,"column":52},"end":{"line":22,"column":77}}]},"8":{"loc":{"start":{"line":22,"column":77},"end":{"line":23,"column":null}},"type":"cond-expr","locations":[{"start":{"line":23,"column":51},"end":{"line":23,"column":null}},{"start":{"line":23,"column":51},"end":{"line":23,"column":null}}]},"9":{"loc":{"start":{"line":22,"column":77},"end":{"line":23,"column":51}},"type":"binary-expr","locations":[{"start":{"line":22,"column":77},"end":{"line":23,"column":null}},{"start":{"line":23,"column":39},"end":{"line":23,"column":51}}]},"10":{"loc":{"start":{"line":23,"column":51},"end":{"line":25,"column":null}},"type":"cond-expr","locations":[{"start":{"line":25,"column":54},"end":{"line":25,"column":null}},{"start":{"line":25,"column":54},"end":{"line":25,"column":null}}]},"11":{"loc":{"start":{"line":23,"column":51},"end":{"line":25,"column":54}},"type":"binary-expr","locations":[{"start":{"line":23,"column":51},"end":{"line":25,"column":null}},{"start":{"line":25,"column":34},"end":{"line":25,"column":54}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":17,"8":17,"9":17,"10":17,"11":17,"12":2,"13":1,"14":1,"15":1,"16":0,"17":0,"18":0,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":0,"26":17,"27":17,"28":17},"f":{"0":17,"1":2,"2":0},"b":{"0":[1,1],"1":[0,0],"2":[17,0],"3":[17,17],"4":[17,0],"5":[17,17],"6":[17,0],"7":[17,17],"8":[17,0],"9":[17,17],"10":[0,17],"11":[17,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/listeners/review-stage.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/listeners/review-stage.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":51}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":52}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":48}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":117}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":89}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":63}},"6":{"start":{"line":14,"column":32},"end":{"line":69,"column":null}},"7":{"start":{"line":15,"column":30},"end":{"line":15,"column":66}},"8":{"start":{"line":18,"column":25},"end":{"line":18,"column":40}},"9":{"start":{"line":19,"column":25},"end":{"line":19,"column":40}},"10":{"start":{"line":33,"column":8},"end":{"line":35,"column":9}},"11":{"start":{"line":34,"column":12},"end":{"line":34,"column":19}},"12":{"start":{"line":37,"column":8},"end":{"line":39,"column":10}},"13":{"start":{"line":40,"column":8},"end":{"line":42,"column":10}},"14":{"start":{"line":44,"column":8},"end":{"line":67,"column":9}},"15":{"start":{"line":45,"column":12},"end":{"line":48,"column":14}},"16":{"start":{"line":50,"column":27},"end":{"line":51,"column":null}},"17":{"start":{"line":54,"column":12},"end":{"line":56,"column":14}},"18":{"start":{"line":58,"column":12},"end":{"line":61,"column":14}},"19":{"start":{"line":63,"column":12},"end":{"line":66,"column":14}},"20":{"start":{"line":14,"column":13},"end":{"line":14,"column":32}},"21":{"start":{"line":30,"column":10},"end":{"line":68,"column":null}},"22":{"start":{"line":14,"column":13},"end":{"line":69,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":17,"column":4},"end":{"line":17,"column":null}},"loc":{"start":{"line":19,"column":53},"end":{"line":20,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":30,"column":10},"end":{"line":30,"column":36}},"loc":{"start":{"line":31,"column":40},"end":{"line":68,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":33,"column":8},"end":{"line":35,"column":9}},"type":"if","locations":[{"start":{"line":33,"column":8},"end":{"line":35,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":31,"column":15},"end":{"line":31,"column":null}},"type":"cond-expr","locations":[{"start":{"line":31,"column":40},"end":{"line":31,"column":null}},{"start":{"line":31,"column":40},"end":{"line":31,"column":null}}]},"2":{"loc":{"start":{"line":31,"column":15},"end":{"line":31,"column":40}},"type":"binary-expr","locations":[{"start":{"line":31,"column":15},"end":{"line":31,"column":null}},{"start":{"line":31,"column":15},"end":{"line":31,"column":40}}]},"3":{"loc":{"start":{"line":32,"column":7},"end":{"line":32,"column":null}},"type":"cond-expr","locations":[{"start":{"line":32,"column":14},"end":{"line":32,"column":null}},{"start":{"line":32,"column":14},"end":{"line":32,"column":null}}]},"4":{"loc":{"start":{"line":32,"column":7},"end":{"line":32,"column":14}},"type":"binary-expr","locations":[{"start":{"line":32,"column":7},"end":{"line":32,"column":null}},{"start":{"line":32,"column":7},"end":{"line":32,"column":14}}]},"5":{"loc":{"start":{"line":18,"column":40},"end":{"line":18,"column":null}},"type":"cond-expr","locations":[{"start":{"line":18,"column":56},"end":{"line":18,"column":null}},{"start":{"line":18,"column":56},"end":{"line":18,"column":null}}]},"6":{"loc":{"start":{"line":18,"column":40},"end":{"line":18,"column":56}},"type":"binary-expr","locations":[{"start":{"line":18,"column":40},"end":{"line":18,"column":null}},{"start":{"line":18,"column":40},"end":{"line":18,"column":56}}]},"7":{"loc":{"start":{"line":18,"column":56},"end":{"line":19,"column":null}},"type":"cond-expr","locations":[{"start":{"line":19,"column":53},"end":{"line":19,"column":null}},{"start":{"line":19,"column":53},"end":{"line":19,"column":null}}]},"8":{"loc":{"start":{"line":18,"column":56},"end":{"line":19,"column":53}},"type":"binary-expr","locations":[{"start":{"line":18,"column":56},"end":{"line":19,"column":null}},{"start":{"line":19,"column":40},"end":{"line":19,"column":53}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":17,"8":17,"9":17,"10":2,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":0,"20":17,"21":17,"22":17},"f":{"0":17,"1":2},"b":{"0":[1,1],"1":[17,0],"2":[17,17],"3":[17,0],"4":[17,17],"5":[17,0],"6":[17,17],"7":[17,0],"8":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-analytics.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-analytics.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":53}},"3":{"start":{"line":11,"column":7},"end":{"line":51,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":36}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":43}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":61}},"7":{"start":{"line":24,"column":26},"end":{"line":24,"column":71}},"8":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"9":{"start":{"line":27,"column":12},"end":{"line":29,"column":14}},"10":{"start":{"line":32,"column":8},"end":{"line":32,"column":25}},"11":{"start":{"line":39,"column":26},"end":{"line":39,"column":59}},"12":{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},"13":{"start":{"line":42,"column":12},"end":{"line":44,"column":14}},"14":{"start":{"line":47,"column":8},"end":{"line":47,"column":71}},"15":{"start":{"line":49,"column":8},"end":{"line":49,"column":25}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":35}},"17":{"start":{"line":11,"column":13},"end":{"line":51,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":59},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":33,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":9}},"loc":{"start":{"line":37,"column":26},"end":{"line":50,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"type":"if","locations":[{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},"type":"if","locations":[{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":14,"column":36},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":65},"end":{"line":14,"column":null}},{"start":{"line":14,"column":65},"end":{"line":14,"column":null}}]},"3":{"loc":{"start":{"line":14,"column":36},"end":{"line":14,"column":65}},"type":"binary-expr","locations":[{"start":{"line":14,"column":36},"end":{"line":14,"column":null}},{"start":{"line":14,"column":36},"end":{"line":14,"column":65}}]},"4":{"loc":{"start":{"line":14,"column":65},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":59},"end":{"line":15,"column":null}},{"start":{"line":15,"column":59},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":14,"column":65},"end":{"line":15,"column":59}},"type":"binary-expr","locations":[{"start":{"line":14,"column":65},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":59}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":4,"7":2,"8":2,"9":0,"10":2,"11":2,"12":2,"13":1,"14":1,"15":1,"16":17,"17":17},"f":{"0":17,"1":4,"2":2},"b":{"0":[0,2],"1":[1,1],"2":[0,17],"3":[17,0],"4":[17,0],"5":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-comment.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-comment.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":53}},"3":{"start":{"line":11,"column":7},"end":{"line":50,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":35}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":43}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":61}},"7":{"start":{"line":24,"column":25},"end":{"line":24,"column":69}},"8":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"9":{"start":{"line":27,"column":12},"end":{"line":29,"column":14}},"10":{"start":{"line":32,"column":8},"end":{"line":32,"column":24}},"11":{"start":{"line":36,"column":24},"end":{"line":36,"column":56}},"12":{"start":{"line":38,"column":8},"end":{"line":42,"column":9}},"13":{"start":{"line":39,"column":12},"end":{"line":41,"column":14}},"14":{"start":{"line":44,"column":8},"end":{"line":44,"column":23}},"15":{"start":{"line":48,"column":8},"end":{"line":48,"column":45}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":33}},"17":{"start":{"line":11,"column":13},"end":{"line":50,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":59},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":33,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":9}},"loc":{"start":{"line":35,"column":28},"end":{"line":45,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":47,"column":4},"end":{"line":47,"column":9}},"loc":{"start":{"line":47,"column":45},"end":{"line":49,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"type":"if","locations":[{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":26,"column":12},"end":{"line":26,"column":46}},"type":"binary-expr","locations":[{"start":{"line":26,"column":12},"end":{"line":26,"column":21}},{"start":{"line":26,"column":25},"end":{"line":26,"column":46}}]},"2":{"loc":{"start":{"line":38,"column":8},"end":{"line":42,"column":9}},"type":"if","locations":[{"start":{"line":38,"column":8},"end":{"line":42,"column":9}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":62},"end":{"line":14,"column":null}},{"start":{"line":14,"column":62},"end":{"line":14,"column":null}}]},"4":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":62}},"type":"binary-expr","locations":[{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},{"start":{"line":14,"column":35},"end":{"line":14,"column":62}}]},"5":{"loc":{"start":{"line":14,"column":62},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":59},"end":{"line":15,"column":null}},{"start":{"line":15,"column":59},"end":{"line":15,"column":null}}]},"6":{"loc":{"start":{"line":14,"column":62},"end":{"line":15,"column":59}},"type":"binary-expr","locations":[{"start":{"line":14,"column":62},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":59}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":3,"7":2,"8":2,"9":1,"10":1,"11":2,"12":2,"13":1,"14":1,"15":4,"16":17,"17":17},"f":{"0":17,"1":3,"2":2,"3":4},"b":{"0":[1,1],"1":[2,2],"2":[1,1],"3":[0,17],"4":[17,0],"5":[17,0],"6":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-insight.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-insight.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":53}},"3":{"start":{"line":11,"column":7},"end":{"line":51,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":35}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":43}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":61}},"7":{"start":{"line":24,"column":25},"end":{"line":24,"column":69}},"8":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"9":{"start":{"line":27,"column":12},"end":{"line":29,"column":14}},"10":{"start":{"line":32,"column":8},"end":{"line":32,"column":24}},"11":{"start":{"line":39,"column":24},"end":{"line":39,"column":56}},"12":{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},"13":{"start":{"line":42,"column":12},"end":{"line":44,"column":14}},"14":{"start":{"line":47,"column":8},"end":{"line":47,"column":69}},"15":{"start":{"line":49,"column":8},"end":{"line":49,"column":23}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":33}},"17":{"start":{"line":11,"column":13},"end":{"line":51,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":59},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":33,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":9}},"loc":{"start":{"line":37,"column":26},"end":{"line":50,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"type":"if","locations":[{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},"type":"if","locations":[{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":62},"end":{"line":14,"column":null}},{"start":{"line":14,"column":62},"end":{"line":14,"column":null}}]},"3":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":62}},"type":"binary-expr","locations":[{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},{"start":{"line":14,"column":35},"end":{"line":14,"column":62}}]},"4":{"loc":{"start":{"line":14,"column":62},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":59},"end":{"line":15,"column":null}},{"start":{"line":15,"column":59},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":14,"column":62},"end":{"line":15,"column":59}},"type":"binary-expr","locations":[{"start":{"line":14,"column":62},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":59}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":4,"7":2,"8":2,"9":0,"10":2,"11":2,"12":2,"13":1,"14":1,"15":1,"16":17,"17":17},"f":{"0":17,"1":4,"2":2},"b":{"0":[0,2],"1":[1,1],"2":[0,17],"3":[17,0],"4":[17,0],"5":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/reports.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/reports.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":16,"column":0},"end":{"line":16,"column":null}},"2":{"start":{"line":23,"column":0},"end":{"line":23,"column":33}},"3":{"start":{"line":24,"column":0},"end":{"line":24,"column":null}},"4":{"start":{"line":28,"column":0},"end":{"line":28,"column":null}},"5":{"start":{"line":33,"column":0},"end":{"line":33,"column":null}},"6":{"start":{"line":37,"column":0},"end":{"line":37,"column":null}},"7":{"start":{"line":41,"column":0},"end":{"line":41,"column":null}},"8":{"start":{"line":45,"column":0},"end":{"line":45,"column":null}},"9":{"start":{"line":49,"column":0},"end":{"line":49,"column":null}},"10":{"start":{"line":54,"column":0},"end":{"line":54,"column":86}},"11":{"start":{"line":58,"column":0},"end":{"line":58,"column":null}},"12":{"start":{"line":63,"column":0},"end":{"line":63,"column":77}},"13":{"start":{"line":64,"column":0},"end":{"line":64,"column":73}},"14":{"start":{"line":65,"column":0},"end":{"line":65,"column":58}},"15":{"start":{"line":66,"column":0},"end":{"line":66,"column":null}},"16":{"start":{"line":70,"column":0},"end":{"line":70,"column":null}},"17":{"start":{"line":74,"column":0},"end":{"line":74,"column":null}},"18":{"start":{"line":78,"column":0},"end":{"line":78,"column":null}},"19":{"start":{"line":84,"column":29},"end":{"line":1291,"column":null}},"20":{"start":{"line":85,"column":30},"end":{"line":85,"column":63}},"21":{"start":{"line":89,"column":25},"end":{"line":89,"column":34}},"22":{"start":{"line":91,"column":25},"end":{"line":91,"column":40}},"23":{"start":{"line":93,"column":25},"end":{"line":93,"column":39}},"24":{"start":{"line":95,"column":25},"end":{"line":95,"column":39}},"25":{"start":{"line":97,"column":25},"end":{"line":97,"column":38}},"26":{"start":{"line":99,"column":25},"end":{"line":99,"column":36}},"27":{"start":{"line":101,"column":25},"end":{"line":101,"column":34}},"28":{"start":{"line":103,"column":25},"end":{"line":103,"column":50}},"29":{"start":{"line":105,"column":25},"end":{"line":105,"column":34}},"30":{"start":{"line":107,"column":25},"end":{"line":107,"column":35}},"31":{"start":{"line":109,"column":25},"end":{"line":109,"column":40}},"32":{"start":{"line":111,"column":25},"end":{"line":111,"column":37}},"33":{"start":{"line":114,"column":36},"end":{"line":114,"column":73}},"34":{"start":{"line":120,"column":8},"end":{"line":120,"column":50}},"35":{"start":{"line":121,"column":8},"end":{"line":121,"column":48}},"36":{"start":{"line":129,"column":8},"end":{"line":129,"column":27}},"37":{"start":{"line":129,"column":20},"end":{"line":129,"column":27}},"38":{"start":{"line":132,"column":12},"end":{"line":133,"column":51}},"39":{"start":{"line":135,"column":8},"end":{"line":139,"column":9}},"40":{"start":{"line":136,"column":12},"end":{"line":138,"column":14}},"41":{"start":{"line":143,"column":23},"end":{"line":143,"column":54}},"42":{"start":{"line":145,"column":8},"end":{"line":147,"column":9}},"43":{"start":{"line":146,"column":12},"end":{"line":146,"column":78}},"44":{"start":{"line":149,"column":8},"end":{"line":149,"column":54}},"45":{"start":{"line":151,"column":8},"end":{"line":151,"column":22}},"46":{"start":{"line":163,"column":8},"end":{"line":163,"column":27}},"47":{"start":{"line":163,"column":20},"end":{"line":163,"column":27}},"48":{"start":{"line":166,"column":12},"end":{"line":167,"column":51}},"49":{"start":{"line":169,"column":8},"end":{"line":169,"column":32}},"50":{"start":{"line":169,"column":25},"end":{"line":169,"column":32}},"51":{"start":{"line":171,"column":23},"end":{"line":171,"column":67}},"52":{"start":{"line":173,"column":8},"end":{"line":176,"column":9}},"53":{"start":{"line":174,"column":12},"end":{"line":175,"column":51}},"54":{"start":{"line":178,"column":34},"end":{"line":178,"column":63}},"55":{"start":{"line":179,"column":32},"end":{"line":179,"column":59}},"56":{"start":{"line":181,"column":8},"end":{"line":181,"column":57}},"57":{"start":{"line":181,"column":50},"end":{"line":181,"column":57}},"58":{"start":{"line":183,"column":26},"end":{"line":185,"column":10}},"59":{"start":{"line":187,"column":8},"end":{"line":187,"column":41}},"60":{"start":{"line":187,"column":34},"end":{"line":187,"column":41}},"61":{"start":{"line":189,"column":8},"end":{"line":191,"column":10}},"62":{"start":{"line":198,"column":23},"end":{"line":198,"column":66}},"63":{"start":{"line":199,"column":8},"end":{"line":203,"column":9}},"64":{"start":{"line":200,"column":12},"end":{"line":202,"column":14}},"65":{"start":{"line":205,"column":8},"end":{"line":205,"column":54}},"66":{"start":{"line":207,"column":8},"end":{"line":207,"column":22}},"67":{"start":{"line":217,"column":8},"end":{"line":217,"column":78}},"68":{"start":{"line":219,"column":25},"end":{"line":219,"column":68}},"69":{"start":{"line":220,"column":8},"end":{"line":223,"column":9}},"70":{"start":{"line":221,"column":12},"end":{"line":221,"column":78}},"71":{"start":{"line":222,"column":12},"end":{"line":222,"column":28}},"72":{"start":{"line":225,"column":23},"end":{"line":225,"column":60}},"73":{"start":{"line":226,"column":8},"end":{"line":228,"column":9}},"74":{"start":{"line":227,"column":12},"end":{"line":227,"column":80}},"75":{"start":{"line":230,"column":31},"end":{"line":232,"column":null}},"76":{"start":{"line":234,"column":32},"end":{"line":234,"column":53}},"77":{"start":{"line":235,"column":59},"end":{"line":245,"column":10}},"78":{"start":{"line":238,"column":32},"end":{"line":238,"column":42}},"79":{"start":{"line":241,"column":28},"end":{"line":242,"column":60}},"80":{"start":{"line":247,"column":12},"end":{"line":247,"column":78}},"81":{"start":{"line":249,"column":26},"end":{"line":249,"column":27}},"82":{"start":{"line":250,"column":8},"end":{"line":252,"column":11}},"83":{"start":{"line":251,"column":12},"end":{"line":251,"column":40}},"84":{"start":{"line":254,"column":8},"end":{"line":256,"column":9}},"85":{"start":{"line":255,"column":12},"end":{"line":255,"column":71}},"86":{"start":{"line":258,"column":40},"end":{"line":263,"column":null}},"87":{"start":{"line":260,"column":16},"end":{"line":260,"column":60}},"88":{"start":{"line":261,"column":16},"end":{"line":261,"column":27}},"89":{"start":{"line":266,"column":28},"end":{"line":269,"column":null}},"90":{"start":{"line":271,"column":29},"end":{"line":274,"column":null}},"91":{"start":{"line":277,"column":8},"end":{"line":279,"column":10}},"92":{"start":{"line":281,"column":24},"end":{"line":284,"column":10}},"93":{"start":{"line":286,"column":26},"end":{"line":288,"column":null}},"94":{"start":{"line":291,"column":25},"end":{"line":291,"column":76}},"95":{"start":{"line":293,"column":12},"end":{"line":293,"column":68}},"96":{"start":{"line":295,"column":33},"end":{"line":297,"column":null}},"97":{"start":{"line":300,"column":23},"end":{"line":327,"column":10}},"98":{"start":{"line":329,"column":30},"end":{"line":329,"column":63}},"99":{"start":{"line":331,"column":8},"end":{"line":334,"column":11}},"100":{"start":{"line":336,"column":8},"end":{"line":341,"column":10}},"101":{"start":{"line":343,"column":8},"end":{"line":345,"column":10}},"102":{"start":{"line":347,"column":33},"end":{"line":350,"column":null}},"103":{"start":{"line":353,"column":35},"end":{"line":356,"column":null}},"104":{"start":{"line":359,"column":8},"end":{"line":362,"column":11}},"105":{"start":{"line":364,"column":8},"end":{"line":366,"column":10}},"106":{"start":{"line":368,"column":8},"end":{"line":370,"column":10}},"107":{"start":{"line":372,"column":8},"end":{"line":374,"column":11}},"108":{"start":{"line":376,"column":8},"end":{"line":376,"column":29}},"109":{"start":{"line":396,"column":8},"end":{"line":401,"column":9}},"110":{"start":{"line":397,"column":26},"end":{"line":397,"column":72}},"111":{"start":{"line":398,"column":12},"end":{"line":398,"column":64}},"112":{"start":{"line":400,"column":12},"end":{"line":400,"column":75}},"113":{"start":{"line":406,"column":14},"end":{"line":406,"column":16}},"114":{"start":{"line":408,"column":8},"end":{"line":416,"column":9}},"115":{"start":{"line":413,"column":12},"end":{"line":415,"column":14}},"116":{"start":{"line":418,"column":8},"end":{"line":429,"column":11}},"117":{"start":{"line":419,"column":12},"end":{"line":423,"column":13}},"118":{"start":{"line":422,"column":16},"end":{"line":422,"column":23}},"119":{"start":{"line":425,"column":12},"end":{"line":428,"column":15}},"120":{"start":{"line":431,"column":8},"end":{"line":435,"column":9}},"121":{"start":{"line":431,"column":51},"end":{"line":431,"column":60}},"122":{"start":{"line":432,"column":12},"end":{"line":434,"column":14}},"123":{"start":{"line":451,"column":37},"end":{"line":451,"column":62}},"124":{"start":{"line":452,"column":8},"end":{"line":463,"column":9}},"125":{"start":{"line":453,"column":12},"end":{"line":462,"column":13}},"126":{"start":{"line":458,"column":16},"end":{"line":461,"column":18}},"127":{"start":{"line":465,"column":39},"end":{"line":465,"column":64}},"128":{"start":{"line":466,"column":8},"end":{"line":480,"column":9}},"129":{"start":{"line":467,"column":12},"end":{"line":470,"column":25}},"130":{"start":{"line":470,"column":16},"end":{"line":470,"column":25}},"131":{"start":{"line":471,"column":33},"end":{"line":471,"column":76}},"132":{"start":{"line":472,"column":12},"end":{"line":472,"column":53}},"133":{"start":{"line":472,"column":44},"end":{"line":472,"column":53}},"134":{"start":{"line":473,"column":12},"end":{"line":477,"column":25}},"135":{"start":{"line":477,"column":16},"end":{"line":477,"column":25}},"136":{"start":{"line":478,"column":28},"end":{"line":478,"column":73}},"137":{"start":{"line":479,"column":12},"end":{"line":479,"column":66}},"138":{"start":{"line":482,"column":8},"end":{"line":519,"column":9}},"139":{"start":{"line":483,"column":33},"end":{"line":483,"column":54}},"140":{"start":{"line":484,"column":12},"end":{"line":484,"column":78}},"141":{"start":{"line":484,"column":69},"end":{"line":484,"column":78}},"142":{"start":{"line":486,"column":31},"end":{"line":488,"column":null}},"143":{"start":{"line":490,"column":33},"end":{"line":490,"column":62}},"144":{"start":{"line":491,"column":12},"end":{"line":491,"column":48}},"145":{"start":{"line":491,"column":39},"end":{"line":491,"column":48}},"146":{"start":{"line":493,"column":29},"end":{"line":493,"column":73}},"147":{"start":{"line":494,"column":12},"end":{"line":494,"column":43}},"148":{"start":{"line":494,"column":34},"end":{"line":494,"column":43}},"149":{"start":{"line":496,"column":35},"end":{"line":498,"column":null}},"150":{"start":{"line":500,"column":12},"end":{"line":505,"column":13}},"151":{"start":{"line":501,"column":16},"end":{"line":503,"column":18}},"152":{"start":{"line":504,"column":16},"end":{"line":504,"column":25}},"153":{"start":{"line":507,"column":33},"end":{"line":507,"column":78}},"154":{"start":{"line":509,"column":12},"end":{"line":518,"column":14}},"155":{"start":{"line":532,"column":8},"end":{"line":532,"column":70}},"156":{"start":{"line":545,"column":8},"end":{"line":558,"column":9}},"157":{"start":{"line":546,"column":26},"end":{"line":546,"column":57}},"158":{"start":{"line":547,"column":26},"end":{"line":547,"column":57}},"159":{"start":{"line":548,"column":31},"end":{"line":549,"column":null}},"160":{"start":{"line":551,"column":31},"end":{"line":551,"column":64}},"161":{"start":{"line":552,"column":31},"end":{"line":554,"column":39}},"162":{"start":{"line":555,"column":12},"end":{"line":557,"column":13}},"163":{"start":{"line":556,"column":16},"end":{"line":556,"column":31}},"164":{"start":{"line":559,"column":8},"end":{"line":559,"column":20}},"165":{"start":{"line":571,"column":29},"end":{"line":573,"column":68}},"166":{"start":{"line":572,"column":50},"end":{"line":572,"column":79}},"167":{"start":{"line":573,"column":25},"end":{"line":573,"column":66}},"168":{"start":{"line":574,"column":8},"end":{"line":574,"column":51}},"169":{"start":{"line":574,"column":39},"end":{"line":574,"column":51}},"170":{"start":{"line":575,"column":20},"end":{"line":577,"column":null}},"171":{"start":{"line":576,"column":28},"end":{"line":576,"column":43}},"172":{"start":{"line":579,"column":8},"end":{"line":579,"column":50}},"173":{"start":{"line":590,"column":8},"end":{"line":590,"column":63}},"174":{"start":{"line":590,"column":51},"end":{"line":590,"column":63}},"175":{"start":{"line":592,"column":12},"end":{"line":592,"column":65}},"176":{"start":{"line":593,"column":8},"end":{"line":593,"column":47}},"177":{"start":{"line":609,"column":36},"end":{"line":610,"column":null}},"178":{"start":{"line":610,"column":28},"end":{"line":610,"column":60}},"179":{"start":{"line":613,"column":8},"end":{"line":615,"column":9}},"180":{"start":{"line":614,"column":12},"end":{"line":614,"column":24}},"181":{"start":{"line":617,"column":8},"end":{"line":619,"column":9}},"182":{"start":{"line":618,"column":12},"end":{"line":618,"column":34}},"183":{"start":{"line":621,"column":24},"end":{"line":623,"column":23}},"184":{"start":{"line":625,"column":8},"end":{"line":625,"column":42}},"185":{"start":{"line":645,"column":35},"end":{"line":646,"column":null}},"186":{"start":{"line":646,"column":19},"end":{"line":646,"column":62}},"187":{"start":{"line":649,"column":34},"end":{"line":651,"column":null}},"188":{"start":{"line":653,"column":59},"end":{"line":653,"column":61}},"189":{"start":{"line":654,"column":39},"end":{"line":654,"column":79}},"190":{"start":{"line":655,"column":48},"end":{"line":655,"column":50}},"191":{"start":{"line":656,"column":48},"end":{"line":656,"column":50}},"192":{"start":{"line":657,"column":49},"end":{"line":657,"column":51}},"193":{"start":{"line":659,"column":8},"end":{"line":757,"column":9}},"194":{"start":{"line":661,"column":16},"end":{"line":661,"column":64}},"195":{"start":{"line":663,"column":34},"end":{"line":665,"column":null}},"196":{"start":{"line":667,"column":34},"end":{"line":669,"column":null}},"197":{"start":{"line":671,"column":35},"end":{"line":673,"column":null}},"198":{"start":{"line":676,"column":12},"end":{"line":678,"column":13}},"199":{"start":{"line":677,"column":16},"end":{"line":677,"column":57}},"200":{"start":{"line":679,"column":12},"end":{"line":681,"column":13}},"201":{"start":{"line":680,"column":16},"end":{"line":680,"column":57}},"202":{"start":{"line":682,"column":12},"end":{"line":684,"column":13}},"203":{"start":{"line":683,"column":16},"end":{"line":683,"column":59}},"204":{"start":{"line":686,"column":37},"end":{"line":688,"column":null}},"205":{"start":{"line":690,"column":37},"end":{"line":692,"column":null}},"206":{"start":{"line":694,"column":38},"end":{"line":696,"column":null}},"207":{"start":{"line":699,"column":42},"end":{"line":701,"column":null}},"208":{"start":{"line":703,"column":43},"end":{"line":705,"column":null}},"209":{"start":{"line":708,"column":12},"end":{"line":731,"column":14}},"210":{"start":{"line":733,"column":33},"end":{"line":733,"column":54}},"211":{"start":{"line":734,"column":12},"end":{"line":756,"column":13}},"212":{"start":{"line":735,"column":36},"end":{"line":743,"column":18}},"213":{"start":{"line":745,"column":16},"end":{"line":747,"column":17}},"214":{"start":{"line":746,"column":20},"end":{"line":746,"column":63}},"215":{"start":{"line":748,"column":16},"end":{"line":750,"column":17}},"216":{"start":{"line":749,"column":20},"end":{"line":749,"column":63}},"217":{"start":{"line":751,"column":16},"end":{"line":753,"column":17}},"218":{"start":{"line":752,"column":20},"end":{"line":752,"column":65}},"219":{"start":{"line":755,"column":16},"end":{"line":755,"column":70}},"220":{"start":{"line":759,"column":61},"end":{"line":759,"column":63}},"221":{"start":{"line":760,"column":50},"end":{"line":760,"column":52}},"222":{"start":{"line":761,"column":50},"end":{"line":761,"column":52}},"223":{"start":{"line":762,"column":51},"end":{"line":762,"column":53}},"224":{"start":{"line":764,"column":8},"end":{"line":831,"column":9}},"225":{"start":{"line":765,"column":34},"end":{"line":766,"column":null}},"226":{"start":{"line":766,"column":50},"end":{"line":766,"column":64}},"227":{"start":{"line":768,"column":34},"end":{"line":769,"column":null}},"228":{"start":{"line":769,"column":50},"end":{"line":769,"column":64}},"229":{"start":{"line":771,"column":35},"end":{"line":772,"column":null}},"230":{"start":{"line":772,"column":51},"end":{"line":772,"column":65}},"231":{"start":{"line":775,"column":12},"end":{"line":777,"column":13}},"232":{"start":{"line":776,"column":16},"end":{"line":776,"column":59}},"233":{"start":{"line":778,"column":12},"end":{"line":780,"column":13}},"234":{"start":{"line":779,"column":16},"end":{"line":779,"column":59}},"235":{"start":{"line":781,"column":12},"end":{"line":783,"column":13}},"236":{"start":{"line":782,"column":16},"end":{"line":782,"column":61}},"237":{"start":{"line":785,"column":37},"end":{"line":787,"column":null}},"238":{"start":{"line":789,"column":37},"end":{"line":791,"column":null}},"239":{"start":{"line":793,"column":38},"end":{"line":795,"column":null}},"240":{"start":{"line":798,"column":42},"end":{"line":800,"column":null}},"241":{"start":{"line":802,"column":43},"end":{"line":804,"column":null}},"242":{"start":{"line":807,"column":12},"end":{"line":830,"column":14}},"243":{"start":{"line":833,"column":8},"end":{"line":848,"column":10}},"244":{"start":{"line":861,"column":35},"end":{"line":867,"column":null}},"245":{"start":{"line":865,"column":24},"end":{"line":865,"column":74}},"246":{"start":{"line":867,"column":35},"end":{"line":867,"column":54}},"247":{"start":{"line":869,"column":24},"end":{"line":869,"column":57}},"248":{"start":{"line":870,"column":8},"end":{"line":877,"column":9}},"249":{"start":{"line":871,"column":12},"end":{"line":873,"column":13}},"250":{"start":{"line":872,"column":16},"end":{"line":872,"column":25}},"251":{"start":{"line":874,"column":27},"end":{"line":874,"column":63}},"252":{"start":{"line":875,"column":12},"end":{"line":875,"column":32}},"253":{"start":{"line":876,"column":12},"end":{"line":876,"column":51}},"254":{"start":{"line":878,"column":8},"end":{"line":878,"column":23}},"255":{"start":{"line":891,"column":23},"end":{"line":898,"column":65}},"256":{"start":{"line":894,"column":20},"end":{"line":896,"column":55}},"257":{"start":{"line":898,"column":29},"end":{"line":898,"column":64}},"258":{"start":{"line":899,"column":8},"end":{"line":899,"column":45}},"259":{"start":{"line":913,"column":8},"end":{"line":915,"column":9}},"260":{"start":{"line":914,"column":12},"end":{"line":914,"column":24}},"261":{"start":{"line":916,"column":8},"end":{"line":916,"column":57}},"262":{"start":{"line":929,"column":30},"end":{"line":930,"column":null}},"263":{"start":{"line":930,"column":36},"end":{"line":930,"column":64}},"264":{"start":{"line":932,"column":30},"end":{"line":933,"column":null}},"265":{"start":{"line":933,"column":36},"end":{"line":933,"column":54}},"266":{"start":{"line":935,"column":31},"end":{"line":936,"column":null}},"267":{"start":{"line":936,"column":36},"end":{"line":936,"column":55}},"268":{"start":{"line":939,"column":33},"end":{"line":941,"column":null}},"269":{"start":{"line":943,"column":33},"end":{"line":945,"column":null}},"270":{"start":{"line":947,"column":34},"end":{"line":949,"column":null}},"271":{"start":{"line":952,"column":38},"end":{"line":954,"column":null}},"272":{"start":{"line":956,"column":39},"end":{"line":958,"column":null}},"273":{"start":{"line":960,"column":8},"end":{"line":971,"column":10}},"274":{"start":{"line":985,"column":8},"end":{"line":987,"column":9}},"275":{"start":{"line":986,"column":12},"end":{"line":986,"column":24}},"276":{"start":{"line":988,"column":8},"end":{"line":988,"column":71}},"277":{"start":{"line":997,"column":8},"end":{"line":997,"column":40}},"278":{"start":{"line":997,"column":28},"end":{"line":997,"column":40}},"279":{"start":{"line":998,"column":8},"end":{"line":998,"column":60}},"280":{"start":{"line":1015,"column":30},"end":{"line":1015,"column":63}},"281":{"start":{"line":1016,"column":30},"end":{"line":1016,"column":63}},"282":{"start":{"line":1017,"column":31},"end":{"line":1017,"column":65}},"283":{"start":{"line":1019,"column":33},"end":{"line":1021,"column":null}},"284":{"start":{"line":1023,"column":33},"end":{"line":1025,"column":null}},"285":{"start":{"line":1027,"column":34},"end":{"line":1029,"column":null}},"286":{"start":{"line":1032,"column":38},"end":{"line":1034,"column":null}},"287":{"start":{"line":1036,"column":39},"end":{"line":1038,"column":null}},"288":{"start":{"line":1041,"column":8},"end":{"line":1052,"column":10}},"289":{"start":{"line":1067,"column":53},"end":{"line":1104,"column":14}},"290":{"start":{"line":1070,"column":20},"end":{"line":1072,"column":51}},"291":{"start":{"line":1074,"column":20},"end":{"line":1076,"column":51}},"292":{"start":{"line":1077,"column":16},"end":{"line":1077,"column":50}},"293":{"start":{"line":1080,"column":16},"end":{"line":1103,"column":18}},"294":{"start":{"line":1106,"column":29},"end":{"line":1108,"column":70}},"295":{"start":{"line":1107,"column":24},"end":{"line":1107,"column":39}},"296":{"start":{"line":1108,"column":40},"end":{"line":1108,"column":69}},"297":{"start":{"line":1109,"column":28},"end":{"line":1111,"column":70}},"298":{"start":{"line":1110,"column":24},"end":{"line":1110,"column":38}},"299":{"start":{"line":1111,"column":40},"end":{"line":1111,"column":69}},"300":{"start":{"line":1114,"column":12},"end":{"line":1114,"column":70}},"301":{"start":{"line":1116,"column":12},"end":{"line":1116,"column":70}},"302":{"start":{"line":1118,"column":12},"end":{"line":1118,"column":68}},"303":{"start":{"line":1120,"column":12},"end":{"line":1120,"column":68}},"304":{"start":{"line":1122,"column":58},"end":{"line":1147,"column":10}},"305":{"start":{"line":1126,"column":26},"end":{"line":1126,"column":77}},"306":{"start":{"line":1132,"column":26},"end":{"line":1132,"column":77}},"307":{"start":{"line":1138,"column":26},"end":{"line":1138,"column":75}},"308":{"start":{"line":1144,"column":26},"end":{"line":1144,"column":75}},"309":{"start":{"line":1149,"column":48},"end":{"line":1149,"column":50}},"310":{"start":{"line":1151,"column":8},"end":{"line":1177,"column":10}},"311":{"start":{"line":1179,"column":8},"end":{"line":1205,"column":10}},"312":{"start":{"line":1207,"column":8},"end":{"line":1233,"column":10}},"313":{"start":{"line":1235,"column":8},"end":{"line":1261,"column":10}},"314":{"start":{"line":1263,"column":8},"end":{"line":1263,"column":24}},"315":{"start":{"line":1271,"column":30},"end":{"line":1277,"column":5}},"316":{"start":{"line":1272,"column":28},"end":{"line":1273,"column":null}},"317":{"start":{"line":1273,"column":37},"end":{"line":1273,"column":70}},"318":{"start":{"line":1275,"column":8},"end":{"line":1275,"column":50}},"319":{"start":{"line":1275,"column":38},"end":{"line":1275,"column":50}},"320":{"start":{"line":1276,"column":8},"end":{"line":1276,"column":64}},"321":{"start":{"line":1285,"column":18},"end":{"line":1285,"column":28}},"322":{"start":{"line":1286,"column":8},"end":{"line":1288,"column":9}},"323":{"start":{"line":1287,"column":12},"end":{"line":1287,"column":34}},"324":{"start":{"line":1289,"column":8},"end":{"line":1289,"column":39}},"325":{"start":{"line":84,"column":13},"end":{"line":84,"column":29}},"326":{"start":{"line":84,"column":13},"end":{"line":1291,"column":null}}},"fnMap":{"0":{"name":"(anonymous_5)","decl":{"start":{"line":87,"column":4},"end":{"line":87,"column":null}},"loc":{"start":{"line":111,"column":56},"end":{"line":112,"column":8}}},"1":{"name":"(anonymous_6)","decl":{"start":{"line":116,"column":4},"end":{"line":116,"column":9}},"loc":{"start":{"line":118,"column":26},"end":{"line":122,"column":5}}},"2":{"name":"(anonymous_7)","decl":{"start":{"line":128,"column":4},"end":{"line":128,"column":9}},"loc":{"start":{"line":128,"column":52},"end":{"line":140,"column":5}}},"3":{"name":"(anonymous_8)","decl":{"start":{"line":142,"column":4},"end":{"line":142,"column":9}},"loc":{"start":{"line":142,"column":48},"end":{"line":152,"column":5}}},"4":{"name":"(anonymous_9)","decl":{"start":{"line":159,"column":4},"end":{"line":159,"column":9}},"loc":{"start":{"line":161,"column":26},"end":{"line":192,"column":5}}},"5":{"name":"(anonymous_10)","decl":{"start":{"line":194,"column":4},"end":{"line":194,"column":9}},"loc":{"start":{"line":196,"column":26},"end":{"line":208,"column":5}}},"6":{"name":"(anonymous_11)","decl":{"start":{"line":216,"column":4},"end":{"line":216,"column":9}},"loc":{"start":{"line":216,"column":50},"end":{"line":377,"column":5}}},"7":{"name":"(anonymous_12)","decl":{"start":{"line":238,"column":25},"end":{"line":238,"column":26}},"loc":{"start":{"line":238,"column":32},"end":{"line":238,"column":42}}},"8":{"name":"(anonymous_13)","decl":{"start":{"line":240,"column":24},"end":{"line":240,"column":25}},"loc":{"start":{"line":241,"column":28},"end":{"line":242,"column":60}}},"9":{"name":"(anonymous_14)","decl":{"start":{"line":250,"column":30},"end":{"line":250,"column":31}},"loc":{"start":{"line":250,"column":39},"end":{"line":252,"column":9}}},"10":{"name":"(anonymous_15)","decl":{"start":{"line":259,"column":12},"end":{"line":259,"column":13}},"loc":{"start":{"line":259,"column":26},"end":{"line":262,"column":13}}},"11":{"name":"(anonymous_16)","decl":{"start":{"line":387,"column":12},"end":{"line":387,"column":17}},"loc":{"start":{"line":392,"column":11},"end":{"line":436,"column":5}}},"12":{"name":"(anonymous_17)","decl":{"start":{"line":418,"column":30},"end":{"line":418,"column":31}},"loc":{"start":{"line":418,"column":39},"end":{"line":429,"column":9}}},"13":{"name":"(anonymous_18)","decl":{"start":{"line":431,"column":41},"end":{"line":431,"column":42}},"loc":{"start":{"line":431,"column":51},"end":{"line":431,"column":60}}},"14":{"name":"(anonymous_19)","decl":{"start":{"line":445,"column":12},"end":{"line":445,"column":17}},"loc":{"start":{"line":449,"column":28},"end":{"line":520,"column":5}}},"15":{"name":"(anonymous_20)","decl":{"start":{"line":528,"column":12},"end":{"line":528,"column":41}},"loc":{"start":{"line":530,"column":38},"end":{"line":533,"column":5}}},"16":{"name":"(anonymous_21)","decl":{"start":{"line":541,"column":12},"end":{"line":541,"column":31}},"loc":{"start":{"line":543,"column":33},"end":{"line":560,"column":5}}},"17":{"name":"(anonymous_22)","decl":{"start":{"line":568,"column":12},"end":{"line":568,"column":28}},"loc":{"start":{"line":569,"column":56},"end":{"line":580,"column":5}}},"18":{"name":"(anonymous_23)","decl":{"start":{"line":572,"column":20},"end":{"line":572,"column":21}},"loc":{"start":{"line":572,"column":50},"end":{"line":572,"column":79}}},"19":{"name":"(anonymous_24)","decl":{"start":{"line":573,"column":17},"end":{"line":573,"column":18}},"loc":{"start":{"line":573,"column":25},"end":{"line":573,"column":66}}},"20":{"name":"(anonymous_25)","decl":{"start":{"line":576,"column":12},"end":{"line":576,"column":13}},"loc":{"start":{"line":576,"column":28},"end":{"line":576,"column":43}}},"21":{"name":"(anonymous_26)","decl":{"start":{"line":587,"column":12},"end":{"line":587,"column":24}},"loc":{"start":{"line":588,"column":50},"end":{"line":594,"column":5}}},"22":{"name":"(anonymous_27)","decl":{"start":{"line":604,"column":12},"end":{"line":604,"column":28}},"loc":{"start":{"line":607,"column":36},"end":{"line":626,"column":5}}},"23":{"name":"(anonymous_28)","decl":{"start":{"line":610,"column":12},"end":{"line":610,"column":13}},"loc":{"start":{"line":610,"column":28},"end":{"line":610,"column":60}}},"24":{"name":"(anonymous_29)","decl":{"start":{"line":635,"column":12},"end":{"line":635,"column":33}},"loc":{"start":{"line":638,"column":25},"end":{"line":849,"column":5}}},"25":{"name":"(anonymous_30)","decl":{"start":{"line":646,"column":12},"end":{"line":646,"column":13}},"loc":{"start":{"line":646,"column":19},"end":{"line":646,"column":62}}},"26":{"name":"(anonymous_31)","decl":{"start":{"line":766,"column":43},"end":{"line":766,"column":44}},"loc":{"start":{"line":766,"column":50},"end":{"line":766,"column":64}}},"27":{"name":"(anonymous_32)","decl":{"start":{"line":769,"column":43},"end":{"line":769,"column":44}},"loc":{"start":{"line":769,"column":50},"end":{"line":769,"column":64}}},"28":{"name":"(anonymous_33)","decl":{"start":{"line":772,"column":44},"end":{"line":772,"column":45}},"loc":{"start":{"line":772,"column":51},"end":{"line":772,"column":65}}},"29":{"name":"(anonymous_34)","decl":{"start":{"line":857,"column":12},"end":{"line":857,"column":34}},"loc":{"start":{"line":859,"column":49},"end":{"line":879,"column":5}}},"30":{"name":"(anonymous_35)","decl":{"start":{"line":864,"column":20},"end":{"line":864,"column":21}},"loc":{"start":{"line":865,"column":24},"end":{"line":865,"column":74}}},"31":{"name":"(anonymous_36)","decl":{"start":{"line":867,"column":21},"end":{"line":867,"column":22}},"loc":{"start":{"line":867,"column":35},"end":{"line":867,"column":54}}},"32":{"name":"(anonymous_37)","decl":{"start":{"line":887,"column":12},"end":{"line":887,"column":38}},"loc":{"start":{"line":889,"column":36},"end":{"line":900,"column":5}}},"33":{"name":"(anonymous_38)","decl":{"start":{"line":893,"column":16},"end":{"line":893,"column":17}},"loc":{"start":{"line":894,"column":20},"end":{"line":896,"column":55}}},"34":{"name":"(anonymous_39)","decl":{"start":{"line":898,"column":17},"end":{"line":898,"column":18}},"loc":{"start":{"line":898,"column":29},"end":{"line":898,"column":64}}},"35":{"name":"(anonymous_40)","decl":{"start":{"line":909,"column":12},"end":{"line":909,"column":32}},"loc":{"start":{"line":911,"column":28},"end":{"line":917,"column":5}}},"36":{"name":"(anonymous_41)","decl":{"start":{"line":925,"column":12},"end":{"line":925,"column":44}},"loc":{"start":{"line":927,"column":25},"end":{"line":972,"column":5}}},"37":{"name":"(anonymous_42)","decl":{"start":{"line":930,"column":26},"end":{"line":930,"column":27}},"loc":{"start":{"line":930,"column":36},"end":{"line":930,"column":64}}},"38":{"name":"(anonymous_43)","decl":{"start":{"line":933,"column":26},"end":{"line":933,"column":27}},"loc":{"start":{"line":933,"column":36},"end":{"line":933,"column":54}}},"39":{"name":"(anonymous_44)","decl":{"start":{"line":936,"column":26},"end":{"line":936,"column":27}},"loc":{"start":{"line":936,"column":36},"end":{"line":936,"column":55}}},"40":{"name":"(anonymous_45)","decl":{"start":{"line":981,"column":12},"end":{"line":981,"column":31}},"loc":{"start":{"line":983,"column":25},"end":{"line":989,"column":5}}},"41":{"name":"(anonymous_46)","decl":{"start":{"line":996,"column":12},"end":{"line":996,"column":27}},"loc":{"start":{"line":996,"column":49},"end":{"line":999,"column":5}}},"42":{"name":"(anonymous_47)","decl":{"start":{"line":1009,"column":12},"end":{"line":1009,"column":44}},"loc":{"start":{"line":1013,"column":25},"end":{"line":1053,"column":5}}},"43":{"name":"(anonymous_48)","decl":{"start":{"line":1062,"column":12},"end":{"line":1062,"column":35}},"loc":{"start":{"line":1065,"column":24},"end":{"line":1264,"column":5}}},"44":{"name":"(anonymous_49)","decl":{"start":{"line":1068,"column":18},"end":{"line":1068,"column":19}},"loc":{"start":{"line":1068,"column":27},"end":{"line":1078,"column":13}}},"45":{"name":"(anonymous_50)","decl":{"start":{"line":1079,"column":17},"end":{"line":1079,"column":18}},"loc":{"start":{"line":1079,"column":35},"end":{"line":1104,"column":13}}},"46":{"name":"(anonymous_51)","decl":{"start":{"line":1107,"column":17},"end":{"line":1107,"column":18}},"loc":{"start":{"line":1107,"column":24},"end":{"line":1107,"column":39}}},"47":{"name":"(anonymous_52)","decl":{"start":{"line":1108,"column":20},"end":{"line":1108,"column":21}},"loc":{"start":{"line":1108,"column":40},"end":{"line":1108,"column":69}}},"48":{"name":"(anonymous_53)","decl":{"start":{"line":1110,"column":17},"end":{"line":1110,"column":18}},"loc":{"start":{"line":1110,"column":24},"end":{"line":1110,"column":38}}},"49":{"name":"(anonymous_54)","decl":{"start":{"line":1111,"column":20},"end":{"line":1111,"column":21}},"loc":{"start":{"line":1111,"column":40},"end":{"line":1111,"column":69}}},"50":{"name":"(anonymous_55)","decl":{"start":{"line":1125,"column":36},"end":{"line":1125,"column":37}},"loc":{"start":{"line":1126,"column":26},"end":{"line":1126,"column":77}}},"51":{"name":"(anonymous_56)","decl":{"start":{"line":1131,"column":36},"end":{"line":1131,"column":37}},"loc":{"start":{"line":1132,"column":26},"end":{"line":1132,"column":77}}},"52":{"name":"(anonymous_57)","decl":{"start":{"line":1137,"column":36},"end":{"line":1137,"column":37}},"loc":{"start":{"line":1138,"column":26},"end":{"line":1138,"column":75}}},"53":{"name":"(anonymous_58)","decl":{"start":{"line":1143,"column":36},"end":{"line":1143,"column":37}},"loc":{"start":{"line":1144,"column":26},"end":{"line":1144,"column":75}}},"54":{"name":"(anonymous_59)","decl":{"start":{"line":1271,"column":30},"end":{"line":1271,"column":31}},"loc":{"start":{"line":1271,"column":70},"end":{"line":1277,"column":5}}},"55":{"name":"(anonymous_60)","decl":{"start":{"line":1273,"column":12},"end":{"line":1273,"column":13}},"loc":{"start":{"line":1273,"column":37},"end":{"line":1273,"column":70}}},"56":{"name":"(anonymous_61)","decl":{"start":{"line":1284,"column":12},"end":{"line":1284,"column":42}},"loc":{"start":{"line":1284,"column":61},"end":{"line":1290,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":129,"column":8},"end":{"line":129,"column":27}},"type":"if","locations":[{"start":{"line":129,"column":8},"end":{"line":129,"column":27}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":132,"column":12},"end":{"line":133,"column":51}},"type":"binary-expr","locations":[{"start":{"line":132,"column":12},"end":{"line":132,"column":54}},{"start":{"line":133,"column":12},"end":{"line":133,"column":51}}]},"2":{"loc":{"start":{"line":135,"column":8},"end":{"line":139,"column":9}},"type":"if","locations":[{"start":{"line":135,"column":8},"end":{"line":139,"column":9}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":145,"column":8},"end":{"line":147,"column":9}},"type":"if","locations":[{"start":{"line":145,"column":8},"end":{"line":147,"column":9}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":163,"column":8},"end":{"line":163,"column":27}},"type":"if","locations":[{"start":{"line":163,"column":8},"end":{"line":163,"column":27}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":166,"column":12},"end":{"line":167,"column":51}},"type":"binary-expr","locations":[{"start":{"line":166,"column":12},"end":{"line":166,"column":54}},{"start":{"line":167,"column":12},"end":{"line":167,"column":51}}]},"6":{"loc":{"start":{"line":169,"column":8},"end":{"line":169,"column":32}},"type":"if","locations":[{"start":{"line":169,"column":8},"end":{"line":169,"column":32}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":173,"column":8},"end":{"line":176,"column":9}},"type":"if","locations":[{"start":{"line":173,"column":8},"end":{"line":176,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":181,"column":8},"end":{"line":181,"column":57}},"type":"if","locations":[{"start":{"line":181,"column":8},"end":{"line":181,"column":57}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":181,"column":12},"end":{"line":181,"column":48}},"type":"binary-expr","locations":[{"start":{"line":181,"column":12},"end":{"line":181,"column":29}},{"start":{"line":181,"column":33},"end":{"line":181,"column":48}}]},"10":{"loc":{"start":{"line":187,"column":8},"end":{"line":187,"column":41}},"type":"if","locations":[{"start":{"line":187,"column":8},"end":{"line":187,"column":41}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":199,"column":8},"end":{"line":203,"column":9}},"type":"if","locations":[{"start":{"line":199,"column":8},"end":{"line":203,"column":9}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":220,"column":8},"end":{"line":223,"column":9}},"type":"if","locations":[{"start":{"line":220,"column":8},"end":{"line":223,"column":9}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":226,"column":8},"end":{"line":228,"column":9}},"type":"if","locations":[{"start":{"line":226,"column":8},"end":{"line":228,"column":9}},{"start":{},"end":{}}]},"14":{"loc":{"start":{"line":254,"column":8},"end":{"line":256,"column":9}},"type":"if","locations":[{"start":{"line":254,"column":8},"end":{"line":256,"column":9}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":268,"column":12},"end":{"line":268,"column":65}},"type":"binary-expr","locations":[{"start":{"line":268,"column":12},"end":{"line":268,"column":60}},{"start":{"line":268,"column":64},"end":{"line":268,"column":65}}]},"16":{"loc":{"start":{"line":273,"column":12},"end":{"line":273,"column":66}},"type":"binary-expr","locations":[{"start":{"line":273,"column":12},"end":{"line":273,"column":61}},{"start":{"line":273,"column":65},"end":{"line":273,"column":66}}]},"17":{"loc":{"start":{"line":396,"column":8},"end":{"line":401,"column":9}},"type":"if","locations":[{"start":{"line":396,"column":8},"end":{"line":401,"column":9}},{"start":{"line":399,"column":15},"end":{"line":401,"column":9}}]},"18":{"loc":{"start":{"line":408,"column":8},"end":{"line":416,"column":9}},"type":"if","locations":[{"start":{"line":408,"column":8},"end":{"line":416,"column":9}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":409,"column":12},"end":{"line":411,"column":50}},"type":"binary-expr","locations":[{"start":{"line":409,"column":12},"end":{"line":409,"column":38}},{"start":{"line":410,"column":12},"end":{"line":411,"column":50}}]},"20":{"loc":{"start":{"line":419,"column":12},"end":{"line":423,"column":13}},"type":"if","locations":[{"start":{"line":419,"column":12},"end":{"line":423,"column":13}},{"start":{},"end":{}}]},"21":{"loc":{"start":{"line":431,"column":8},"end":{"line":435,"column":9}},"type":"if","locations":[{"start":{"line":431,"column":8},"end":{"line":435,"column":9}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":453,"column":12},"end":{"line":462,"column":13}},"type":"if","locations":[{"start":{"line":453,"column":12},"end":{"line":462,"column":13}},{"start":{},"end":{}}]},"23":{"loc":{"start":{"line":454,"column":16},"end":{"line":456,"column":51}},"type":"binary-expr","locations":[{"start":{"line":454,"column":16},"end":{"line":454,"column":66}},{"start":{"line":455,"column":16},"end":{"line":455,"column":46}},{"start":{"line":456,"column":16},"end":{"line":456,"column":51}}]},"24":{"loc":{"start":{"line":467,"column":12},"end":{"line":470,"column":25}},"type":"if","locations":[{"start":{"line":467,"column":12},"end":{"line":470,"column":25}},{"start":{},"end":{}}]},"25":{"loc":{"start":{"line":472,"column":12},"end":{"line":472,"column":53}},"type":"if","locations":[{"start":{"line":472,"column":12},"end":{"line":472,"column":53}},{"start":{},"end":{}}]},"26":{"loc":{"start":{"line":473,"column":12},"end":{"line":477,"column":25}},"type":"if","locations":[{"start":{"line":473,"column":12},"end":{"line":477,"column":25}},{"start":{},"end":{}}]},"27":{"loc":{"start":{"line":474,"column":16},"end":{"line":475,"column":51}},"type":"binary-expr","locations":[{"start":{"line":474,"column":16},"end":{"line":474,"column":46}},{"start":{"line":475,"column":16},"end":{"line":475,"column":51}}]},"28":{"loc":{"start":{"line":478,"column":28},"end":{"line":478,"column":73}},"type":"binary-expr","locations":[{"start":{"line":478,"column":28},"end":{"line":478,"column":68}},{"start":{"line":478,"column":72},"end":{"line":478,"column":73}}]},"29":{"loc":{"start":{"line":484,"column":12},"end":{"line":484,"column":78}},"type":"if","locations":[{"start":{"line":484,"column":12},"end":{"line":484,"column":78}},{"start":{},"end":{}}]},"30":{"loc":{"start":{"line":484,"column":16},"end":{"line":484,"column":67}},"type":"binary-expr","locations":[{"start":{"line":484,"column":16},"end":{"line":484,"column":37}},{"start":{"line":484,"column":41},"end":{"line":484,"column":67}}]},"31":{"loc":{"start":{"line":487,"column":16},"end":{"line":487,"column":46}},"type":"binary-expr","locations":[{"start":{"line":487,"column":16},"end":{"line":487,"column":38}},{"start":{"line":487,"column":42},"end":{"line":487,"column":46}}]},"32":{"loc":{"start":{"line":488,"column":16},"end":{"line":488,"column":47}},"type":"binary-expr","locations":[{"start":{"line":488,"column":16},"end":{"line":488,"column":39}},{"start":{"line":488,"column":43},"end":{"line":488,"column":47}}]},"33":{"loc":{"start":{"line":491,"column":12},"end":{"line":491,"column":48}},"type":"if","locations":[{"start":{"line":491,"column":12},"end":{"line":491,"column":48}},{"start":{},"end":{}}]},"34":{"loc":{"start":{"line":494,"column":12},"end":{"line":494,"column":43}},"type":"if","locations":[{"start":{"line":494,"column":12},"end":{"line":494,"column":43}},{"start":{},"end":{}}]},"35":{"loc":{"start":{"line":500,"column":12},"end":{"line":505,"column":13}},"type":"if","locations":[{"start":{"line":500,"column":12},"end":{"line":505,"column":13}},{"start":{},"end":{}}]},"36":{"loc":{"start":{"line":507,"column":33},"end":{"line":507,"column":78}},"type":"binary-expr","locations":[{"start":{"line":507,"column":33},"end":{"line":507,"column":73}},{"start":{"line":507,"column":77},"end":{"line":507,"column":78}}]},"37":{"loc":{"start":{"line":511,"column":29},"end":{"line":511,"column":51}},"type":"binary-expr","locations":[{"start":{"line":511,"column":29},"end":{"line":511,"column":43}},{"start":{"line":511,"column":47},"end":{"line":511,"column":51}}]},"38":{"loc":{"start":{"line":552,"column":31},"end":{"line":554,"column":39}},"type":"cond-expr","locations":[{"start":{"line":553,"column":18},"end":{"line":553,"column":48}},{"start":{"line":554,"column":18},"end":{"line":554,"column":39}}]},"39":{"loc":{"start":{"line":555,"column":12},"end":{"line":557,"column":13}},"type":"if","locations":[{"start":{"line":555,"column":12},"end":{"line":557,"column":13}},{"start":{},"end":{}}]},"40":{"loc":{"start":{"line":555,"column":16},"end":{"line":555,"column":40}},"type":"binary-expr","locations":[{"start":{"line":555,"column":16},"end":{"line":555,"column":26}},{"start":{"line":555,"column":30},"end":{"line":555,"column":40}}]},"41":{"loc":{"start":{"line":572,"column":50},"end":{"line":572,"column":79}},"type":"binary-expr","locations":[{"start":{"line":572,"column":50},"end":{"line":572,"column":60}},{"start":{"line":572,"column":64},"end":{"line":572,"column":79}}]},"42":{"loc":{"start":{"line":573,"column":25},"end":{"line":573,"column":66}},"type":"cond-expr","locations":[{"start":{"line":573,"column":48},"end":{"line":573,"column":49}},{"start":{"line":573,"column":52},"end":{"line":573,"column":66}}]},"43":{"loc":{"start":{"line":574,"column":8},"end":{"line":574,"column":51}},"type":"if","locations":[{"start":{"line":574,"column":8},"end":{"line":574,"column":51}},{"start":{},"end":{}}]},"44":{"loc":{"start":{"line":590,"column":8},"end":{"line":590,"column":63}},"type":"if","locations":[{"start":{"line":590,"column":8},"end":{"line":590,"column":63}},{"start":{},"end":{}}]},"45":{"loc":{"start":{"line":590,"column":12},"end":{"line":590,"column":49}},"type":"binary-expr","locations":[{"start":{"line":590,"column":12},"end":{"line":590,"column":26}},{"start":{"line":590,"column":30},"end":{"line":590,"column":49}}]},"46":{"loc":{"start":{"line":592,"column":12},"end":{"line":592,"column":65}},"type":"cond-expr","locations":[{"start":{"line":592,"column":39},"end":{"line":592,"column":44}},{"start":{"line":592,"column":47},"end":{"line":592,"column":65}}]},"47":{"loc":{"start":{"line":613,"column":8},"end":{"line":615,"column":9}},"type":"if","locations":[{"start":{"line":613,"column":8},"end":{"line":615,"column":9}},{"start":{},"end":{}}]},"48":{"loc":{"start":{"line":617,"column":8},"end":{"line":619,"column":9}},"type":"if","locations":[{"start":{"line":617,"column":8},"end":{"line":619,"column":9}},{"start":{},"end":{}}]},"49":{"loc":{"start":{"line":661,"column":16},"end":{"line":661,"column":64}},"type":"binary-expr","locations":[{"start":{"line":661,"column":16},"end":{"line":661,"column":58}},{"start":{"line":661,"column":62},"end":{"line":661,"column":64}}]},"50":{"loc":{"start":{"line":676,"column":12},"end":{"line":678,"column":13}},"type":"if","locations":[{"start":{"line":676,"column":12},"end":{"line":678,"column":13}},{"start":{},"end":{}}]},"51":{"loc":{"start":{"line":679,"column":12},"end":{"line":681,"column":13}},"type":"if","locations":[{"start":{"line":679,"column":12},"end":{"line":681,"column":13}},{"start":{},"end":{}}]},"52":{"loc":{"start":{"line":682,"column":12},"end":{"line":684,"column":13}},"type":"if","locations":[{"start":{"line":682,"column":12},"end":{"line":684,"column":13}},{"start":{},"end":{}}]},"53":{"loc":{"start":{"line":717,"column":24},"end":{"line":717,"column":64}},"type":"binary-expr","locations":[{"start":{"line":717,"column":24},"end":{"line":717,"column":56}},{"start":{"line":717,"column":60},"end":{"line":717,"column":64}}]},"54":{"loc":{"start":{"line":718,"column":35},"end":{"line":718,"column":75}},"type":"binary-expr","locations":[{"start":{"line":718,"column":35},"end":{"line":718,"column":67}},{"start":{"line":718,"column":71},"end":{"line":718,"column":75}}]},"55":{"loc":{"start":{"line":719,"column":36},"end":{"line":719,"column":77}},"type":"binary-expr","locations":[{"start":{"line":719,"column":36},"end":{"line":719,"column":69}},{"start":{"line":719,"column":73},"end":{"line":719,"column":77}}]},"56":{"loc":{"start":{"line":721,"column":24},"end":{"line":721,"column":67}},"type":"binary-expr","locations":[{"start":{"line":721,"column":24},"end":{"line":721,"column":59}},{"start":{"line":721,"column":63},"end":{"line":721,"column":67}}]},"57":{"loc":{"start":{"line":723,"column":24},"end":{"line":723,"column":67}},"type":"binary-expr","locations":[{"start":{"line":723,"column":24},"end":{"line":723,"column":59}},{"start":{"line":723,"column":63},"end":{"line":723,"column":67}}]},"58":{"loc":{"start":{"line":725,"column":24},"end":{"line":725,"column":68}},"type":"binary-expr","locations":[{"start":{"line":725,"column":24},"end":{"line":725,"column":60}},{"start":{"line":725,"column":64},"end":{"line":725,"column":68}}]},"59":{"loc":{"start":{"line":727,"column":24},"end":{"line":727,"column":72}},"type":"binary-expr","locations":[{"start":{"line":727,"column":24},"end":{"line":727,"column":64}},{"start":{"line":727,"column":68},"end":{"line":727,"column":72}}]},"60":{"loc":{"start":{"line":729,"column":24},"end":{"line":729,"column":73}},"type":"binary-expr","locations":[{"start":{"line":729,"column":24},"end":{"line":729,"column":65}},{"start":{"line":729,"column":69},"end":{"line":729,"column":73}}]},"61":{"loc":{"start":{"line":734,"column":12},"end":{"line":756,"column":13}},"type":"if","locations":[{"start":{"line":734,"column":12},"end":{"line":756,"column":13}},{"start":{},"end":{}}]},"62":{"loc":{"start":{"line":734,"column":16},"end":{"line":734,"column":67}},"type":"binary-expr","locations":[{"start":{"line":734,"column":16},"end":{"line":734,"column":37}},{"start":{"line":734,"column":41},"end":{"line":734,"column":67}}]},"63":{"loc":{"start":{"line":735,"column":36},"end":{"line":743,"column":18}},"type":"binary-expr","locations":[{"start":{"line":735,"column":36},"end":{"line":736,"column":null}},{"start":{"line":737,"column":21},"end":{"line":743,"column":18}}]},"64":{"loc":{"start":{"line":739,"column":37},"end":{"line":739,"column":69}},"type":"binary-expr","locations":[{"start":{"line":739,"column":37},"end":{"line":739,"column":61}},{"start":{"line":739,"column":65},"end":{"line":739,"column":69}}]},"65":{"loc":{"start":{"line":745,"column":16},"end":{"line":747,"column":17}},"type":"if","locations":[{"start":{"line":745,"column":16},"end":{"line":747,"column":17}},{"start":{},"end":{}}]},"66":{"loc":{"start":{"line":748,"column":16},"end":{"line":750,"column":17}},"type":"if","locations":[{"start":{"line":748,"column":16},"end":{"line":750,"column":17}},{"start":{},"end":{}}]},"67":{"loc":{"start":{"line":751,"column":16},"end":{"line":753,"column":17}},"type":"if","locations":[{"start":{"line":751,"column":16},"end":{"line":753,"column":17}},{"start":{},"end":{}}]},"68":{"loc":{"start":{"line":775,"column":12},"end":{"line":777,"column":13}},"type":"if","locations":[{"start":{"line":775,"column":12},"end":{"line":777,"column":13}},{"start":{},"end":{}}]},"69":{"loc":{"start":{"line":778,"column":12},"end":{"line":780,"column":13}},"type":"if","locations":[{"start":{"line":778,"column":12},"end":{"line":780,"column":13}},{"start":{},"end":{}}]},"70":{"loc":{"start":{"line":781,"column":12},"end":{"line":783,"column":13}},"type":"if","locations":[{"start":{"line":781,"column":12},"end":{"line":783,"column":13}},{"start":{},"end":{}}]},"71":{"loc":{"start":{"line":816,"column":24},"end":{"line":816,"column":64}},"type":"binary-expr","locations":[{"start":{"line":816,"column":24},"end":{"line":816,"column":56}},{"start":{"line":816,"column":60},"end":{"line":816,"column":64}}]},"72":{"loc":{"start":{"line":817,"column":35},"end":{"line":817,"column":75}},"type":"binary-expr","locations":[{"start":{"line":817,"column":35},"end":{"line":817,"column":67}},{"start":{"line":817,"column":71},"end":{"line":817,"column":75}}]},"73":{"loc":{"start":{"line":818,"column":36},"end":{"line":818,"column":77}},"type":"binary-expr","locations":[{"start":{"line":818,"column":36},"end":{"line":818,"column":69}},{"start":{"line":818,"column":73},"end":{"line":818,"column":77}}]},"74":{"loc":{"start":{"line":820,"column":24},"end":{"line":820,"column":67}},"type":"binary-expr","locations":[{"start":{"line":820,"column":24},"end":{"line":820,"column":59}},{"start":{"line":820,"column":63},"end":{"line":820,"column":67}}]},"75":{"loc":{"start":{"line":822,"column":24},"end":{"line":822,"column":67}},"type":"binary-expr","locations":[{"start":{"line":822,"column":24},"end":{"line":822,"column":59}},{"start":{"line":822,"column":63},"end":{"line":822,"column":67}}]},"76":{"loc":{"start":{"line":824,"column":24},"end":{"line":824,"column":68}},"type":"binary-expr","locations":[{"start":{"line":824,"column":24},"end":{"line":824,"column":60}},{"start":{"line":824,"column":64},"end":{"line":824,"column":68}}]},"77":{"loc":{"start":{"line":826,"column":24},"end":{"line":826,"column":72}},"type":"binary-expr","locations":[{"start":{"line":826,"column":24},"end":{"line":826,"column":64}},{"start":{"line":826,"column":68},"end":{"line":826,"column":72}}]},"78":{"loc":{"start":{"line":828,"column":24},"end":{"line":828,"column":73}},"type":"binary-expr","locations":[{"start":{"line":828,"column":24},"end":{"line":828,"column":65}},{"start":{"line":828,"column":69},"end":{"line":828,"column":73}}]},"79":{"loc":{"start":{"line":871,"column":12},"end":{"line":873,"column":13}},"type":"if","locations":[{"start":{"line":871,"column":12},"end":{"line":873,"column":13}},{"start":{},"end":{}}]},"80":{"loc":{"start":{"line":874,"column":27},"end":{"line":874,"column":63}},"type":"binary-expr","locations":[{"start":{"line":874,"column":27},"end":{"line":874,"column":57}},{"start":{"line":874,"column":61},"end":{"line":874,"column":63}}]},"81":{"loc":{"start":{"line":894,"column":20},"end":{"line":896,"column":55}},"type":"binary-expr","locations":[{"start":{"line":894,"column":20},"end":{"line":894,"column":58}},{"start":{"line":895,"column":20},"end":{"line":895,"column":50}},{"start":{"line":896,"column":20},"end":{"line":896,"column":55}}]},"82":{"loc":{"start":{"line":913,"column":8},"end":{"line":915,"column":9}},"type":"if","locations":[{"start":{"line":913,"column":8},"end":{"line":915,"column":9}},{"start":{},"end":{}}]},"83":{"loc":{"start":{"line":913,"column":12},"end":{"line":913,"column":48}},"type":"binary-expr","locations":[{"start":{"line":913,"column":12},"end":{"line":913,"column":31}},{"start":{"line":913,"column":35},"end":{"line":913,"column":48}}]},"84":{"loc":{"start":{"line":985,"column":8},"end":{"line":987,"column":9}},"type":"if","locations":[{"start":{"line":985,"column":8},"end":{"line":987,"column":9}},{"start":{},"end":{}}]},"85":{"loc":{"start":{"line":985,"column":12},"end":{"line":985,"column":47}},"type":"binary-expr","locations":[{"start":{"line":985,"column":12},"end":{"line":985,"column":26}},{"start":{"line":985,"column":30},"end":{"line":985,"column":47}}]},"86":{"loc":{"start":{"line":997,"column":8},"end":{"line":997,"column":40}},"type":"if","locations":[{"start":{"line":997,"column":8},"end":{"line":997,"column":40}},{"start":{},"end":{}}]},"87":{"loc":{"start":{"line":1070,"column":20},"end":{"line":1072,"column":51}},"type":"cond-expr","locations":[{"start":{"line":1071,"column":27},"end":{"line":1071,"column":48}},{"start":{"line":1072,"column":27},"end":{"line":1072,"column":50}}]},"88":{"loc":{"start":{"line":1071,"column":27},"end":{"line":1071,"column":48}},"type":"binary-expr","locations":[{"start":{"line":1071,"column":27},"end":{"line":1071,"column":42}},{"start":{"line":1071,"column":46},"end":{"line":1071,"column":48}}]},"89":{"loc":{"start":{"line":1072,"column":27},"end":{"line":1072,"column":50}},"type":"binary-expr","locations":[{"start":{"line":1072,"column":27},"end":{"line":1072,"column":44}},{"start":{"line":1072,"column":48},"end":{"line":1072,"column":50}}]},"90":{"loc":{"start":{"line":1074,"column":20},"end":{"line":1076,"column":51}},"type":"cond-expr","locations":[{"start":{"line":1075,"column":27},"end":{"line":1075,"column":48}},{"start":{"line":1076,"column":27},"end":{"line":1076,"column":50}}]},"91":{"loc":{"start":{"line":1075,"column":27},"end":{"line":1075,"column":48}},"type":"binary-expr","locations":[{"start":{"line":1075,"column":27},"end":{"line":1075,"column":42}},{"start":{"line":1075,"column":46},"end":{"line":1075,"column":48}}]},"92":{"loc":{"start":{"line":1076,"column":27},"end":{"line":1076,"column":50}},"type":"binary-expr","locations":[{"start":{"line":1076,"column":27},"end":{"line":1076,"column":44}},{"start":{"line":1076,"column":48},"end":{"line":1076,"column":50}}]},"93":{"loc":{"start":{"line":1082,"column":24},"end":{"line":1084,"column":49}},"type":"cond-expr","locations":[{"start":{"line":1083,"column":30},"end":{"line":1083,"column":47}},{"start":{"line":1084,"column":30},"end":{"line":1084,"column":49}}]},"94":{"loc":{"start":{"line":1087,"column":24},"end":{"line":1089,"column":59}},"type":"binary-expr","locations":[{"start":{"line":1087,"column":25},"end":{"line":1089,"column":52}},{"start":{"line":1089,"column":57},"end":{"line":1089,"column":59}}]},"95":{"loc":{"start":{"line":1087,"column":25},"end":{"line":1089,"column":52}},"type":"cond-expr","locations":[{"start":{"line":1088,"column":30},"end":{"line":1088,"column":50}},{"start":{"line":1089,"column":30},"end":{"line":1089,"column":52}}]},"96":{"loc":{"start":{"line":1108,"column":40},"end":{"line":1108,"column":69}},"type":"binary-expr","locations":[{"start":{"line":1108,"column":40},"end":{"line":1108,"column":55}},{"start":{"line":1108,"column":59},"end":{"line":1108,"column":69}}]},"97":{"loc":{"start":{"line":1111,"column":40},"end":{"line":1111,"column":69}},"type":"binary-expr","locations":[{"start":{"line":1111,"column":40},"end":{"line":1111,"column":55}},{"start":{"line":1111,"column":59},"end":{"line":1111,"column":69}}]},"98":{"loc":{"start":{"line":1114,"column":12},"end":{"line":1114,"column":70}},"type":"cond-expr","locations":[{"start":{"line":1114,"column":38},"end":{"line":1114,"column":63}},{"start":{"line":1114,"column":66},"end":{"line":1114,"column":70}}]},"99":{"loc":{"start":{"line":1116,"column":12},"end":{"line":1116,"column":70}},"type":"cond-expr","locations":[{"start":{"line":1116,"column":38},"end":{"line":1116,"column":63}},{"start":{"line":1116,"column":66},"end":{"line":1116,"column":70}}]},"100":{"loc":{"start":{"line":1118,"column":12},"end":{"line":1118,"column":68}},"type":"cond-expr","locations":[{"start":{"line":1118,"column":37},"end":{"line":1118,"column":61}},{"start":{"line":1118,"column":64},"end":{"line":1118,"column":68}}]},"101":{"loc":{"start":{"line":1120,"column":12},"end":{"line":1120,"column":68}},"type":"cond-expr","locations":[{"start":{"line":1120,"column":37},"end":{"line":1120,"column":61}},{"start":{"line":1120,"column":64},"end":{"line":1120,"column":68}}]},"102":{"loc":{"start":{"line":1124,"column":16},"end":{"line":1128,"column":31}},"type":"cond-expr","locations":[{"start":{"line":1125,"column":22},"end":{"line":1126,"column":null}},{"start":{"line":1128,"column":22},"end":{"line":1128,"column":31}}]},"103":{"loc":{"start":{"line":1126,"column":38},"end":{"line":1126,"column":58}},"type":"binary-expr","locations":[{"start":{"line":1126,"column":38},"end":{"line":1126,"column":53}},{"start":{"line":1126,"column":57},"end":{"line":1126,"column":58}}]},"104":{"loc":{"start":{"line":1130,"column":16},"end":{"line":1134,"column":31}},"type":"cond-expr","locations":[{"start":{"line":1131,"column":22},"end":{"line":1132,"column":null}},{"start":{"line":1134,"column":22},"end":{"line":1134,"column":31}}]},"105":{"loc":{"start":{"line":1132,"column":38},"end":{"line":1132,"column":58}},"type":"binary-expr","locations":[{"start":{"line":1132,"column":38},"end":{"line":1132,"column":53}},{"start":{"line":1132,"column":57},"end":{"line":1132,"column":58}}]},"106":{"loc":{"start":{"line":1136,"column":16},"end":{"line":1140,"column":31}},"type":"cond-expr","locations":[{"start":{"line":1137,"column":22},"end":{"line":1138,"column":null}},{"start":{"line":1140,"column":22},"end":{"line":1140,"column":31}}]},"107":{"loc":{"start":{"line":1138,"column":38},"end":{"line":1138,"column":57}},"type":"binary-expr","locations":[{"start":{"line":1138,"column":38},"end":{"line":1138,"column":52}},{"start":{"line":1138,"column":56},"end":{"line":1138,"column":57}}]},"108":{"loc":{"start":{"line":1142,"column":16},"end":{"line":1146,"column":31}},"type":"cond-expr","locations":[{"start":{"line":1143,"column":22},"end":{"line":1144,"column":null}},{"start":{"line":1146,"column":22},"end":{"line":1146,"column":31}}]},"109":{"loc":{"start":{"line":1144,"column":38},"end":{"line":1144,"column":57}},"type":"binary-expr","locations":[{"start":{"line":1144,"column":38},"end":{"line":1144,"column":52}},{"start":{"line":1144,"column":56},"end":{"line":1144,"column":57}}]},"110":{"loc":{"start":{"line":1157,"column":20},"end":{"line":1159,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1158,"column":27},"end":{"line":1158,"column":75}},{"start":{"line":1159,"column":26},"end":{"line":1159,"column":30}}]},"111":{"loc":{"start":{"line":1158,"column":27},"end":{"line":1158,"column":75}},"type":"binary-expr","locations":[{"start":{"line":1158,"column":27},"end":{"line":1158,"column":67}},{"start":{"line":1158,"column":71},"end":{"line":1158,"column":75}}]},"112":{"loc":{"start":{"line":1161,"column":20},"end":{"line":1163,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1162,"column":27},"end":{"line":1162,"column":78}},{"start":{"line":1163,"column":26},"end":{"line":1163,"column":30}}]},"113":{"loc":{"start":{"line":1162,"column":27},"end":{"line":1162,"column":78}},"type":"binary-expr","locations":[{"start":{"line":1162,"column":27},"end":{"line":1162,"column":70}},{"start":{"line":1162,"column":74},"end":{"line":1162,"column":78}}]},"114":{"loc":{"start":{"line":1165,"column":20},"end":{"line":1167,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1166,"column":27},"end":{"line":1166,"column":75}},{"start":{"line":1167,"column":26},"end":{"line":1167,"column":30}}]},"115":{"loc":{"start":{"line":1166,"column":27},"end":{"line":1166,"column":75}},"type":"binary-expr","locations":[{"start":{"line":1166,"column":27},"end":{"line":1166,"column":67}},{"start":{"line":1166,"column":71},"end":{"line":1166,"column":75}}]},"116":{"loc":{"start":{"line":1169,"column":20},"end":{"line":1171,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1170,"column":27},"end":{"line":1170,"column":78}},{"start":{"line":1171,"column":26},"end":{"line":1171,"column":30}}]},"117":{"loc":{"start":{"line":1170,"column":27},"end":{"line":1170,"column":78}},"type":"binary-expr","locations":[{"start":{"line":1170,"column":27},"end":{"line":1170,"column":70}},{"start":{"line":1170,"column":74},"end":{"line":1170,"column":78}}]},"118":{"loc":{"start":{"line":1185,"column":20},"end":{"line":1187,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1186,"column":27},"end":{"line":1186,"column":74}},{"start":{"line":1187,"column":26},"end":{"line":1187,"column":30}}]},"119":{"loc":{"start":{"line":1186,"column":27},"end":{"line":1186,"column":74}},"type":"binary-expr","locations":[{"start":{"line":1186,"column":27},"end":{"line":1186,"column":66}},{"start":{"line":1186,"column":70},"end":{"line":1186,"column":74}}]},"120":{"loc":{"start":{"line":1189,"column":20},"end":{"line":1191,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1190,"column":27},"end":{"line":1190,"column":77}},{"start":{"line":1191,"column":26},"end":{"line":1191,"column":30}}]},"121":{"loc":{"start":{"line":1190,"column":27},"end":{"line":1190,"column":77}},"type":"binary-expr","locations":[{"start":{"line":1190,"column":27},"end":{"line":1190,"column":69}},{"start":{"line":1190,"column":73},"end":{"line":1190,"column":77}}]},"122":{"loc":{"start":{"line":1193,"column":20},"end":{"line":1195,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1194,"column":27},"end":{"line":1194,"column":74}},{"start":{"line":1195,"column":26},"end":{"line":1195,"column":30}}]},"123":{"loc":{"start":{"line":1194,"column":27},"end":{"line":1194,"column":74}},"type":"binary-expr","locations":[{"start":{"line":1194,"column":27},"end":{"line":1194,"column":66}},{"start":{"line":1194,"column":70},"end":{"line":1194,"column":74}}]},"124":{"loc":{"start":{"line":1197,"column":20},"end":{"line":1199,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1198,"column":27},"end":{"line":1198,"column":77}},{"start":{"line":1199,"column":26},"end":{"line":1199,"column":30}}]},"125":{"loc":{"start":{"line":1198,"column":27},"end":{"line":1198,"column":77}},"type":"binary-expr","locations":[{"start":{"line":1198,"column":27},"end":{"line":1198,"column":69}},{"start":{"line":1198,"column":73},"end":{"line":1198,"column":77}}]},"126":{"loc":{"start":{"line":1213,"column":20},"end":{"line":1215,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1214,"column":27},"end":{"line":1214,"column":74}},{"start":{"line":1215,"column":26},"end":{"line":1215,"column":30}}]},"127":{"loc":{"start":{"line":1214,"column":27},"end":{"line":1214,"column":74}},"type":"binary-expr","locations":[{"start":{"line":1214,"column":27},"end":{"line":1214,"column":66}},{"start":{"line":1214,"column":70},"end":{"line":1214,"column":74}}]},"128":{"loc":{"start":{"line":1217,"column":20},"end":{"line":1219,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1218,"column":27},"end":{"line":1218,"column":77}},{"start":{"line":1219,"column":26},"end":{"line":1219,"column":30}}]},"129":{"loc":{"start":{"line":1218,"column":27},"end":{"line":1218,"column":77}},"type":"binary-expr","locations":[{"start":{"line":1218,"column":27},"end":{"line":1218,"column":69}},{"start":{"line":1218,"column":73},"end":{"line":1218,"column":77}}]},"130":{"loc":{"start":{"line":1221,"column":20},"end":{"line":1223,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1222,"column":27},"end":{"line":1222,"column":74}},{"start":{"line":1223,"column":26},"end":{"line":1223,"column":30}}]},"131":{"loc":{"start":{"line":1222,"column":27},"end":{"line":1222,"column":74}},"type":"binary-expr","locations":[{"start":{"line":1222,"column":27},"end":{"line":1222,"column":66}},{"start":{"line":1222,"column":70},"end":{"line":1222,"column":74}}]},"132":{"loc":{"start":{"line":1225,"column":20},"end":{"line":1227,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1226,"column":27},"end":{"line":1226,"column":77}},{"start":{"line":1227,"column":26},"end":{"line":1227,"column":30}}]},"133":{"loc":{"start":{"line":1226,"column":27},"end":{"line":1226,"column":77}},"type":"binary-expr","locations":[{"start":{"line":1226,"column":27},"end":{"line":1226,"column":69}},{"start":{"line":1226,"column":73},"end":{"line":1226,"column":77}}]},"134":{"loc":{"start":{"line":1241,"column":20},"end":{"line":1243,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1242,"column":27},"end":{"line":1242,"column":73}},{"start":{"line":1243,"column":26},"end":{"line":1243,"column":30}}]},"135":{"loc":{"start":{"line":1242,"column":27},"end":{"line":1242,"column":73}},"type":"binary-expr","locations":[{"start":{"line":1242,"column":27},"end":{"line":1242,"column":65}},{"start":{"line":1242,"column":69},"end":{"line":1242,"column":73}}]},"136":{"loc":{"start":{"line":1245,"column":20},"end":{"line":1247,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1246,"column":27},"end":{"line":1246,"column":76}},{"start":{"line":1247,"column":26},"end":{"line":1247,"column":30}}]},"137":{"loc":{"start":{"line":1246,"column":27},"end":{"line":1246,"column":76}},"type":"binary-expr","locations":[{"start":{"line":1246,"column":27},"end":{"line":1246,"column":68}},{"start":{"line":1246,"column":72},"end":{"line":1246,"column":76}}]},"138":{"loc":{"start":{"line":1249,"column":20},"end":{"line":1251,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1250,"column":27},"end":{"line":1250,"column":73}},{"start":{"line":1251,"column":26},"end":{"line":1251,"column":30}}]},"139":{"loc":{"start":{"line":1250,"column":27},"end":{"line":1250,"column":73}},"type":"binary-expr","locations":[{"start":{"line":1250,"column":27},"end":{"line":1250,"column":65}},{"start":{"line":1250,"column":69},"end":{"line":1250,"column":73}}]},"140":{"loc":{"start":{"line":1253,"column":20},"end":{"line":1255,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1254,"column":27},"end":{"line":1254,"column":76}},{"start":{"line":1255,"column":26},"end":{"line":1255,"column":30}}]},"141":{"loc":{"start":{"line":1254,"column":27},"end":{"line":1254,"column":76}},"type":"binary-expr","locations":[{"start":{"line":1254,"column":27},"end":{"line":1254,"column":68}},{"start":{"line":1254,"column":72},"end":{"line":1254,"column":76}}]},"142":{"loc":{"start":{"line":1273,"column":37},"end":{"line":1273,"column":70}},"type":"binary-expr","locations":[{"start":{"line":1273,"column":37},"end":{"line":1273,"column":49}},{"start":{"line":1273,"column":53},"end":{"line":1273,"column":70}}]},"143":{"loc":{"start":{"line":1275,"column":8},"end":{"line":1275,"column":50}},"type":"if","locations":[{"start":{"line":1275,"column":8},"end":{"line":1275,"column":50}},{"start":{},"end":{}}]},"144":{"loc":{"start":{"line":89,"column":34},"end":{"line":89,"column":null}},"type":"cond-expr","locations":[{"start":{"line":89,"column":54},"end":{"line":89,"column":null}},{"start":{"line":89,"column":54},"end":{"line":89,"column":null}}]},"145":{"loc":{"start":{"line":89,"column":34},"end":{"line":89,"column":54}},"type":"binary-expr","locations":[{"start":{"line":89,"column":34},"end":{"line":89,"column":null}},{"start":{"line":89,"column":34},"end":{"line":89,"column":54}}]},"146":{"loc":{"start":{"line":89,"column":54},"end":{"line":91,"column":null}},"type":"cond-expr","locations":[{"start":{"line":91,"column":69},"end":{"line":91,"column":null}},{"start":{"line":91,"column":69},"end":{"line":91,"column":null}}]},"147":{"loc":{"start":{"line":89,"column":54},"end":{"line":91,"column":69}},"type":"binary-expr","locations":[{"start":{"line":89,"column":54},"end":{"line":91,"column":null}},{"start":{"line":91,"column":40},"end":{"line":91,"column":69}}]},"148":{"loc":{"start":{"line":91,"column":69},"end":{"line":93,"column":null}},"type":"cond-expr","locations":[{"start":{"line":93,"column":66},"end":{"line":93,"column":null}},{"start":{"line":93,"column":66},"end":{"line":93,"column":null}}]},"149":{"loc":{"start":{"line":91,"column":69},"end":{"line":93,"column":66}},"type":"binary-expr","locations":[{"start":{"line":91,"column":69},"end":{"line":93,"column":null}},{"start":{"line":93,"column":39},"end":{"line":93,"column":66}}]},"150":{"loc":{"start":{"line":93,"column":66},"end":{"line":95,"column":null}},"type":"cond-expr","locations":[{"start":{"line":95,"column":66},"end":{"line":95,"column":null}},{"start":{"line":95,"column":66},"end":{"line":95,"column":null}}]},"151":{"loc":{"start":{"line":93,"column":66},"end":{"line":95,"column":66}},"type":"binary-expr","locations":[{"start":{"line":93,"column":66},"end":{"line":95,"column":null}},{"start":{"line":95,"column":39},"end":{"line":95,"column":66}}]},"152":{"loc":{"start":{"line":95,"column":66},"end":{"line":97,"column":null}},"type":"cond-expr","locations":[{"start":{"line":97,"column":62},"end":{"line":97,"column":null}},{"start":{"line":97,"column":62},"end":{"line":97,"column":null}}]},"153":{"loc":{"start":{"line":95,"column":66},"end":{"line":97,"column":62}},"type":"binary-expr","locations":[{"start":{"line":95,"column":66},"end":{"line":97,"column":null}},{"start":{"line":97,"column":38},"end":{"line":97,"column":62}}]},"154":{"loc":{"start":{"line":97,"column":62},"end":{"line":99,"column":null}},"type":"cond-expr","locations":[{"start":{"line":99,"column":58},"end":{"line":99,"column":null}},{"start":{"line":99,"column":58},"end":{"line":99,"column":null}}]},"155":{"loc":{"start":{"line":97,"column":62},"end":{"line":99,"column":58}},"type":"binary-expr","locations":[{"start":{"line":97,"column":62},"end":{"line":99,"column":null}},{"start":{"line":99,"column":36},"end":{"line":99,"column":58}}]},"156":{"loc":{"start":{"line":99,"column":58},"end":{"line":101,"column":null}},"type":"cond-expr","locations":[{"start":{"line":101,"column":54},"end":{"line":101,"column":null}},{"start":{"line":101,"column":54},"end":{"line":101,"column":null}}]},"157":{"loc":{"start":{"line":99,"column":58},"end":{"line":101,"column":54}},"type":"binary-expr","locations":[{"start":{"line":99,"column":58},"end":{"line":101,"column":null}},{"start":{"line":101,"column":34},"end":{"line":101,"column":54}}]},"158":{"loc":{"start":{"line":101,"column":54},"end":{"line":103,"column":null}},"type":"cond-expr","locations":[{"start":{"line":103,"column":86},"end":{"line":103,"column":null}},{"start":{"line":103,"column":86},"end":{"line":103,"column":null}}]},"159":{"loc":{"start":{"line":101,"column":54},"end":{"line":103,"column":86}},"type":"binary-expr","locations":[{"start":{"line":101,"column":54},"end":{"line":103,"column":null}},{"start":{"line":103,"column":50},"end":{"line":103,"column":86}}]},"160":{"loc":{"start":{"line":103,"column":86},"end":{"line":105,"column":null}},"type":"cond-expr","locations":[{"start":{"line":105,"column":54},"end":{"line":105,"column":null}},{"start":{"line":105,"column":54},"end":{"line":105,"column":null}}]},"161":{"loc":{"start":{"line":103,"column":86},"end":{"line":105,"column":54}},"type":"binary-expr","locations":[{"start":{"line":103,"column":86},"end":{"line":105,"column":null}},{"start":{"line":105,"column":34},"end":{"line":105,"column":54}}]},"162":{"loc":{"start":{"line":105,"column":54},"end":{"line":107,"column":null}},"type":"cond-expr","locations":[{"start":{"line":107,"column":56},"end":{"line":107,"column":null}},{"start":{"line":107,"column":56},"end":{"line":107,"column":null}}]},"163":{"loc":{"start":{"line":105,"column":54},"end":{"line":107,"column":56}},"type":"binary-expr","locations":[{"start":{"line":105,"column":54},"end":{"line":107,"column":null}},{"start":{"line":107,"column":35},"end":{"line":107,"column":56}}]},"164":{"loc":{"start":{"line":107,"column":56},"end":{"line":109,"column":null}},"type":"cond-expr","locations":[{"start":{"line":109,"column":66},"end":{"line":109,"column":null}},{"start":{"line":109,"column":66},"end":{"line":109,"column":null}}]},"165":{"loc":{"start":{"line":107,"column":56},"end":{"line":109,"column":66}},"type":"binary-expr","locations":[{"start":{"line":107,"column":56},"end":{"line":109,"column":null}},{"start":{"line":109,"column":40},"end":{"line":109,"column":66}}]},"166":{"loc":{"start":{"line":109,"column":66},"end":{"line":111,"column":null}},"type":"cond-expr","locations":[{"start":{"line":111,"column":56},"end":{"line":111,"column":null}},{"start":{"line":111,"column":56},"end":{"line":111,"column":null}}]},"167":{"loc":{"start":{"line":109,"column":66},"end":{"line":111,"column":56}},"type":"binary-expr","locations":[{"start":{"line":109,"column":66},"end":{"line":111,"column":null}},{"start":{"line":111,"column":37},"end":{"line":111,"column":56}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":17,"8":17,"9":17,"10":17,"11":17,"12":17,"13":17,"14":17,"15":17,"16":17,"17":17,"18":17,"19":17,"20":17,"21":17,"22":17,"23":17,"24":17,"25":17,"26":17,"27":17,"28":17,"29":17,"30":17,"31":17,"32":17,"33":17,"34":3,"35":2,"36":3,"37":1,"38":2,"39":2,"40":1,"41":17,"42":17,"43":4,"44":13,"45":10,"46":14,"47":0,"48":14,"49":14,"50":10,"51":4,"52":4,"53":0,"54":4,"55":4,"56":4,"57":1,"58":3,"59":3,"60":0,"61":3,"62":2,"63":2,"64":1,"65":1,"66":1,"67":4,"68":4,"69":4,"70":1,"71":1,"72":3,"73":3,"74":1,"75":2,"76":2,"77":2,"78":2,"79":1,"80":2,"81":2,"82":2,"83":2,"84":2,"85":0,"86":2,"87":2,"88":2,"89":2,"90":2,"91":2,"92":2,"93":2,"94":2,"95":2,"96":2,"97":2,"98":2,"99":2,"100":2,"101":2,"102":2,"103":2,"104":2,"105":2,"106":2,"107":2,"108":2,"109":0,"110":0,"111":0,"112":0,"113":0,"114":0,"115":0,"116":0,"117":0,"118":0,"119":0,"120":0,"121":0,"122":0,"123":2,"124":2,"125":1,"126":1,"127":2,"128":2,"129":2,"130":0,"131":2,"132":2,"133":0,"134":2,"135":0,"136":2,"137":2,"138":2,"139":1,"140":1,"141":0,"142":1,"143":1,"144":1,"145":0,"146":1,"147":1,"148":1,"149":0,"150":0,"151":0,"152":0,"153":0,"154":0,"155":1,"156":0,"157":0,"158":0,"159":0,"160":0,"161":0,"162":0,"163":0,"164":0,"165":25,"166":13,"167":12,"168":25,"169":14,"170":11,"171":12,"172":11,"173":41,"174":8,"175":33,"176":33,"177":4,"178":4,"179":4,"180":2,"181":2,"182":0,"183":2,"184":2,"185":2,"186":1,"187":2,"188":2,"189":2,"190":2,"191":2,"192":2,"193":2,"194":1,"195":1,"196":1,"197":1,"198":1,"199":0,"200":1,"201":1,"202":1,"203":1,"204":1,"205":1,"206":1,"207":1,"208":1,"209":1,"210":1,"211":1,"212":1,"213":1,"214":0,"215":1,"216":1,"217":1,"218":1,"219":1,"220":2,"221":2,"222":2,"223":2,"224":2,"225":1,"226":0,"227":1,"228":1,"229":1,"230":1,"231":1,"232":0,"233":1,"234":1,"235":1,"236":1,"237":1,"238":1,"239":1,"240":1,"241":1,"242":1,"243":2,"244":2,"245":1,"246":1,"247":2,"248":2,"249":2,"250":0,"251":2,"252":2,"253":2,"254":2,"255":3,"256":6,"257":2,"258":3,"259":16,"260":16,"261":0,"262":2,"263":1,"264":2,"265":1,"266":2,"267":1,"268":2,"269":2,"270":2,"271":2,"272":2,"273":2,"274":24,"275":14,"276":10,"277":48,"278":36,"279":12,"280":4,"281":4,"282":4,"283":4,"284":4,"285":4,"286":4,"287":4,"288":4,"289":4,"290":0,"291":0,"292":0,"293":2,"294":4,"295":2,"296":2,"297":4,"298":2,"299":2,"300":4,"301":4,"302":4,"303":4,"304":4,"305":2,"306":2,"307":0,"308":0,"309":4,"310":4,"311":4,"312":4,"313":4,"314":4,"315":17,"316":6,"317":12,"318":6,"319":2,"320":4,"321":4,"322":4,"323":8,"324":4,"325":17,"326":17},"f":{"0":17,"1":3,"2":3,"3":17,"4":14,"5":2,"6":4,"7":2,"8":1,"9":2,"10":2,"11":0,"12":0,"13":0,"14":2,"15":1,"16":0,"17":25,"18":13,"19":12,"20":12,"21":41,"22":4,"23":4,"24":2,"25":1,"26":0,"27":1,"28":1,"29":2,"30":1,"31":1,"32":3,"33":6,"34":2,"35":16,"36":2,"37":1,"38":1,"39":1,"40":24,"41":48,"42":4,"43":4,"44":0,"45":2,"46":2,"47":2,"48":2,"49":2,"50":2,"51":2,"52":0,"53":0,"54":6,"55":12,"56":4},"b":{"0":[1,2],"1":[2,2],"2":[1,1],"3":[4,13],"4":[0,14],"5":[14,14],"6":[10,4],"7":[0,4],"8":[1,3],"9":[4,4],"10":[0,3],"11":[1,1],"12":[1,3],"13":[1,2],"14":[0,2],"15":[2,1],"16":[2,1],"17":[0,0],"18":[0,0],"19":[0,0],"20":[0,0],"21":[0,0],"22":[1,0],"23":[1,1,1],"24":[0,2],"25":[0,2],"26":[0,2],"27":[2,2],"28":[2,1],"29":[0,1],"30":[1,1],"31":[1,0],"32":[1,0],"33":[0,1],"34":[1,0],"35":[0,0],"36":[0,0],"37":[0,0],"38":[0,0],"39":[0,0],"40":[0,0],"41":[13,12],"42":[12,0],"43":[14,11],"44":[8,33],"45":[41,33],"46":[33,0],"47":[2,2],"48":[0,2],"49":[1,0],"50":[0,1],"51":[1,0],"52":[1,0],"53":[1,1],"54":[1,0],"55":[1,0],"56":[1,1],"57":[1,0],"58":[1,0],"59":[1,1],"60":[1,1],"61":[1,0],"62":[1,1],"63":[1,1],"64":[1,0],"65":[0,1],"66":[1,0],"67":[1,0],"68":[0,1],"69":[1,0],"70":[1,0],"71":[1,1],"72":[1,0],"73":[1,0],"74":[1,1],"75":[1,0],"76":[1,0],"77":[1,1],"78":[1,1],"79":[0,2],"80":[2,1],"81":[6,2,2],"82":[16,0],"83":[16,10],"84":[14,10],"85":[24,10],"86":[36,12],"87":[0,0],"88":[0,0],"89":[0,0],"90":[0,0],"91":[0,0],"92":[0,0],"93":[1,1],"94":[2,0],"95":[1,1],"96":[2,2],"97":[2,2],"98":[2,2],"99":[2,2],"100":[0,4],"101":[0,4],"102":[2,2],"103":[2,0],"104":[2,2],"105":[2,0],"106":[0,4],"107":[0,0],"108":[0,4],"109":[0,0],"110":[2,2],"111":[2,1],"112":[2,2],"113":[2,1],"114":[2,2],"115":[2,1],"116":[2,2],"117":[2,1],"118":[2,2],"119":[2,1],"120":[2,2],"121":[2,1],"122":[2,2],"123":[2,1],"124":[2,2],"125":[2,1],"126":[2,2],"127":[2,2],"128":[2,2],"129":[2,2],"130":[2,2],"131":[2,2],"132":[2,2],"133":[2,2],"134":[2,2],"135":[2,2],"136":[2,2],"137":[2,2],"138":[2,2],"139":[2,2],"140":[2,2],"141":[2,2],"142":[12,8],"143":[2,4],"144":[0,17],"145":[17,0],"146":[0,17],"147":[17,0],"148":[0,17],"149":[17,0],"150":[0,17],"151":[17,0],"152":[0,17],"153":[17,0],"154":[0,17],"155":[17,0],"156":[0,17],"157":[17,0],"158":[0,17],"159":[17,0],"160":[0,17],"161":[17,0],"162":[0,17],"163":[17,0],"164":[0,17],"165":[17,0],"166":[0,17],"167":[17,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/startegic-report-insight.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/startegic-report-insight.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":72}},"3":{"start":{"line":11,"column":7},"end":{"line":52,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":35}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":43}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":70}},"7":{"start":{"line":25,"column":12},"end":{"line":25,"column":74}},"8":{"start":{"line":27,"column":8},"end":{"line":31,"column":9}},"9":{"start":{"line":28,"column":12},"end":{"line":30,"column":14}},"10":{"start":{"line":33,"column":8},"end":{"line":33,"column":24}},"11":{"start":{"line":40,"column":24},"end":{"line":40,"column":56}},"12":{"start":{"line":42,"column":8},"end":{"line":46,"column":9}},"13":{"start":{"line":43,"column":12},"end":{"line":45,"column":14}},"14":{"start":{"line":48,"column":8},"end":{"line":48,"column":78}},"15":{"start":{"line":50,"column":8},"end":{"line":50,"column":23}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":42}},"17":{"start":{"line":11,"column":13},"end":{"line":52,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":68},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":34,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":36,"column":4},"end":{"line":36,"column":9}},"loc":{"start":{"line":38,"column":26},"end":{"line":51,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":27,"column":8},"end":{"line":31,"column":9}},"type":"if","locations":[{"start":{"line":27,"column":8},"end":{"line":31,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":42,"column":8},"end":{"line":46,"column":9}},"type":"if","locations":[{"start":{"line":42,"column":8},"end":{"line":46,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":71},"end":{"line":14,"column":null}},{"start":{"line":14,"column":71},"end":{"line":14,"column":null}}]},"3":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":71}},"type":"binary-expr","locations":[{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},{"start":{"line":14,"column":35},"end":{"line":14,"column":71}}]},"4":{"loc":{"start":{"line":14,"column":71},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":68},"end":{"line":15,"column":null}},{"start":{"line":15,"column":68},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":14,"column":71},"end":{"line":15,"column":68}},"type":"binary-expr","locations":[{"start":{"line":14,"column":71},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":68}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":4,"7":2,"8":2,"9":0,"10":2,"11":2,"12":2,"13":1,"14":1,"15":1,"16":17,"17":17},"f":{"0":17,"1":4,"2":2},"b":{"0":[0,2],"1":[1,1],"2":[0,17],"3":[17,0],"4":[17,0],"5":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/strategic-report-analytics.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/strategic-report-analytics.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":72}},"3":{"start":{"line":11,"column":7},"end":{"line":57,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":45}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":52}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":79}},"7":{"start":{"line":25,"column":12},"end":{"line":26,"column":null}},"8":{"start":{"line":29,"column":8},"end":{"line":33,"column":9}},"9":{"start":{"line":30,"column":12},"end":{"line":32,"column":14}},"10":{"start":{"line":35,"column":8},"end":{"line":35,"column":34}},"11":{"start":{"line":42,"column":35},"end":{"line":42,"column":77}},"12":{"start":{"line":44,"column":8},"end":{"line":48,"column":9}},"13":{"start":{"line":45,"column":12},"end":{"line":47,"column":14}},"14":{"start":{"line":50,"column":8},"end":{"line":53,"column":10}},"15":{"start":{"line":55,"column":8},"end":{"line":55,"column":34}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":44}},"17":{"start":{"line":11,"column":13},"end":{"line":57,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":77},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":36,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":38,"column":4},"end":{"line":38,"column":9}},"loc":{"start":{"line":40,"column":26},"end":{"line":56,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":29,"column":8},"end":{"line":33,"column":9}},"type":"if","locations":[{"start":{"line":29,"column":8},"end":{"line":33,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":44,"column":8},"end":{"line":48,"column":9}},"type":"if","locations":[{"start":{"line":44,"column":8},"end":{"line":48,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":14,"column":45},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":83},"end":{"line":14,"column":null}},{"start":{"line":14,"column":83},"end":{"line":14,"column":null}}]},"3":{"loc":{"start":{"line":14,"column":45},"end":{"line":14,"column":83}},"type":"binary-expr","locations":[{"start":{"line":14,"column":45},"end":{"line":14,"column":null}},{"start":{"line":14,"column":45},"end":{"line":14,"column":83}}]},"4":{"loc":{"start":{"line":14,"column":83},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":77},"end":{"line":15,"column":null}},{"start":{"line":15,"column":77},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":14,"column":83},"end":{"line":15,"column":77}},"type":"binary-expr","locations":[{"start":{"line":14,"column":83},"end":{"line":15,"column":null}},{"start":{"line":15,"column":52},"end":{"line":15,"column":77}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":4,"7":2,"8":2,"9":0,"10":2,"11":2,"12":2,"13":1,"14":1,"15":1,"16":17,"17":17},"f":{"0":17,"1":4,"2":2},"b":{"0":[0,2],"1":[1,1],"2":[0,17],"3":[17,0],"4":[17,0],"5":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/strategic-reports.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/strategic-reports.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":12,"column":0},"end":{"line":12,"column":null}},"2":{"start":{"line":19,"column":0},"end":{"line":19,"column":33}},"3":{"start":{"line":20,"column":0},"end":{"line":20,"column":null}},"4":{"start":{"line":24,"column":0},"end":{"line":24,"column":null}},"5":{"start":{"line":28,"column":0},"end":{"line":28,"column":null}},"6":{"start":{"line":34,"column":0},"end":{"line":34,"column":null}},"7":{"start":{"line":39,"column":0},"end":{"line":39,"column":null}},"8":{"start":{"line":44,"column":0},"end":{"line":44,"column":null}},"9":{"start":{"line":48,"column":0},"end":{"line":48,"column":null}},"10":{"start":{"line":54,"column":0},"end":{"line":54,"column":null}},"11":{"start":{"line":58,"column":0},"end":{"line":58,"column":null}},"12":{"start":{"line":62,"column":0},"end":{"line":62,"column":null}},"13":{"start":{"line":70,"column":0},"end":{"line":70,"column":92}},"14":{"start":{"line":71,"column":0},"end":{"line":71,"column":96}},"15":{"start":{"line":72,"column":0},"end":{"line":72,"column":77}},"16":{"start":{"line":73,"column":0},"end":{"line":73,"column":null}},"17":{"start":{"line":77,"column":0},"end":{"line":77,"column":null}},"18":{"start":{"line":81,"column":0},"end":{"line":81,"column":null}},"19":{"start":{"line":85,"column":0},"end":{"line":85,"column":null}},"20":{"start":{"line":89,"column":0},"end":{"line":89,"column":null}},"21":{"start":{"line":95,"column":38},"end":{"line":956,"column":null}},"22":{"start":{"line":96,"column":30},"end":{"line":96,"column":72}},"23":{"start":{"line":100,"column":25},"end":{"line":100,"column":43}},"24":{"start":{"line":102,"column":25},"end":{"line":102,"column":49}},"25":{"start":{"line":104,"column":25},"end":{"line":104,"column":38}},"26":{"start":{"line":106,"column":25},"end":{"line":106,"column":36}},"27":{"start":{"line":108,"column":25},"end":{"line":108,"column":34}},"28":{"start":{"line":110,"column":25},"end":{"line":110,"column":34}},"29":{"start":{"line":112,"column":25},"end":{"line":112,"column":37}},"30":{"start":{"line":114,"column":25},"end":{"line":114,"column":34}},"31":{"start":{"line":116,"column":25},"end":{"line":116,"column":32}},"32":{"start":{"line":118,"column":25},"end":{"line":118,"column":32}},"33":{"start":{"line":120,"column":25},"end":{"line":120,"column":36}},"34":{"start":{"line":122,"column":25},"end":{"line":122,"column":38}},"35":{"start":{"line":124,"column":25},"end":{"line":124,"column":36}},"36":{"start":{"line":126,"column":25},"end":{"line":126,"column":46}},"37":{"start":{"line":128,"column":25},"end":{"line":128,"column":39}},"38":{"start":{"line":135,"column":8},"end":{"line":135,"column":59}},"39":{"start":{"line":136,"column":8},"end":{"line":136,"column":57}},"40":{"start":{"line":144,"column":8},"end":{"line":144,"column":27}},"41":{"start":{"line":144,"column":20},"end":{"line":144,"column":27}},"42":{"start":{"line":147,"column":12},"end":{"line":148,"column":51}},"43":{"start":{"line":150,"column":8},"end":{"line":154,"column":9}},"44":{"start":{"line":151,"column":12},"end":{"line":153,"column":14}},"45":{"start":{"line":161,"column":23},"end":{"line":161,"column":63}},"46":{"start":{"line":163,"column":8},"end":{"line":167,"column":9}},"47":{"start":{"line":164,"column":12},"end":{"line":166,"column":14}},"48":{"start":{"line":169,"column":8},"end":{"line":169,"column":63}},"49":{"start":{"line":171,"column":8},"end":{"line":171,"column":22}},"50":{"start":{"line":183,"column":8},"end":{"line":183,"column":27}},"51":{"start":{"line":183,"column":20},"end":{"line":183,"column":27}},"52":{"start":{"line":186,"column":12},"end":{"line":187,"column":51}},"53":{"start":{"line":189,"column":8},"end":{"line":189,"column":32}},"54":{"start":{"line":189,"column":25},"end":{"line":189,"column":32}},"55":{"start":{"line":191,"column":24},"end":{"line":191,"column":78}},"56":{"start":{"line":193,"column":8},"end":{"line":196,"column":9}},"57":{"start":{"line":194,"column":12},"end":{"line":195,"column":61}},"58":{"start":{"line":198,"column":34},"end":{"line":199,"column":null}},"59":{"start":{"line":199,"column":24},"end":{"line":199,"column":53}},"60":{"start":{"line":202,"column":8},"end":{"line":202,"column":38}},"61":{"start":{"line":202,"column":31},"end":{"line":202,"column":38}},"62":{"start":{"line":204,"column":8},"end":{"line":206,"column":10}},"63":{"start":{"line":213,"column":23},"end":{"line":213,"column":73}},"64":{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},"65":{"start":{"line":215,"column":12},"end":{"line":217,"column":14}},"66":{"start":{"line":220,"column":8},"end":{"line":220,"column":63}},"67":{"start":{"line":222,"column":8},"end":{"line":222,"column":22}},"68":{"start":{"line":233,"column":8},"end":{"line":233,"column":77}},"69":{"start":{"line":235,"column":22},"end":{"line":235,"column":61}},"70":{"start":{"line":236,"column":8},"end":{"line":238,"column":9}},"71":{"start":{"line":237,"column":12},"end":{"line":237,"column":70}},"72":{"start":{"line":240,"column":25},"end":{"line":240,"column":75}},"73":{"start":{"line":241,"column":8},"end":{"line":244,"column":9}},"74":{"start":{"line":242,"column":12},"end":{"line":242,"column":76}},"75":{"start":{"line":243,"column":12},"end":{"line":243,"column":28}},"76":{"start":{"line":246,"column":24},"end":{"line":246,"column":62}},"77":{"start":{"line":247,"column":8},"end":{"line":251,"column":9}},"78":{"start":{"line":248,"column":12},"end":{"line":250,"column":14}},"79":{"start":{"line":253,"column":27},"end":{"line":253,"column":50}},"80":{"start":{"line":254,"column":26},"end":{"line":254,"column":43}},"81":{"start":{"line":261,"column":14},"end":{"line":261,"column":16}},"82":{"start":{"line":263,"column":8},"end":{"line":280,"column":9}},"83":{"start":{"line":264,"column":27},"end":{"line":264,"column":71}},"84":{"start":{"line":265,"column":12},"end":{"line":268,"column":13}},"85":{"start":{"line":266,"column":16},"end":{"line":266,"column":39}},"86":{"start":{"line":267,"column":16},"end":{"line":267,"column":42}},"87":{"start":{"line":270,"column":12},"end":{"line":279,"column":15}},"88":{"start":{"line":282,"column":26},"end":{"line":282,"column":47}},"89":{"start":{"line":283,"column":31},"end":{"line":283,"column":58}},"90":{"start":{"line":284,"column":27},"end":{"line":284,"column":50}},"91":{"start":{"line":285,"column":65},"end":{"line":289,"column":10}},"92":{"start":{"line":290,"column":29},"end":{"line":290,"column":54}},"93":{"start":{"line":291,"column":25},"end":{"line":291,"column":54}},"94":{"start":{"line":292,"column":29},"end":{"line":292,"column":62}},"95":{"start":{"line":293,"column":31},"end":{"line":293,"column":66}},"96":{"start":{"line":294,"column":29},"end":{"line":294,"column":62}},"97":{"start":{"line":296,"column":8},"end":{"line":415,"column":9}},"98":{"start":{"line":297,"column":28},"end":{"line":297,"column":45}},"99":{"start":{"line":298,"column":32},"end":{"line":298,"column":49}},"100":{"start":{"line":299,"column":34},"end":{"line":299,"column":51}},"101":{"start":{"line":300,"column":32},"end":{"line":300,"column":49}},"102":{"start":{"line":302,"column":26},"end":{"line":302,"column":67}},"103":{"start":{"line":303,"column":12},"end":{"line":313,"column":13}},"104":{"start":{"line":304,"column":16},"end":{"line":304,"column":37}},"105":{"start":{"line":306,"column":16},"end":{"line":308,"column":17}},"106":{"start":{"line":307,"column":20},"end":{"line":307,"column":46}},"107":{"start":{"line":310,"column":16},"end":{"line":312,"column":17}},"108":{"start":{"line":311,"column":20},"end":{"line":311,"column":54}},"109":{"start":{"line":315,"column":32},"end":{"line":317,"column":null}},"110":{"start":{"line":319,"column":12},"end":{"line":325,"column":15}},"111":{"start":{"line":320,"column":16},"end":{"line":320,"column":47}},"112":{"start":{"line":322,"column":16},"end":{"line":324,"column":17}},"113":{"start":{"line":323,"column":20},"end":{"line":323,"column":51}},"114":{"start":{"line":327,"column":12},"end":{"line":334,"column":13}},"115":{"start":{"line":328,"column":16},"end":{"line":333,"column":17}},"116":{"start":{"line":329,"column":33},"end":{"line":329,"column":66}},"117":{"start":{"line":330,"column":20},"end":{"line":332,"column":21}},"118":{"start":{"line":331,"column":24},"end":{"line":331,"column":51}},"119":{"start":{"line":336,"column":12},"end":{"line":343,"column":13}},"120":{"start":{"line":337,"column":16},"end":{"line":342,"column":17}},"121":{"start":{"line":338,"column":37},"end":{"line":338,"column":78}},"122":{"start":{"line":339,"column":20},"end":{"line":341,"column":21}},"123":{"start":{"line":340,"column":24},"end":{"line":340,"column":63}},"124":{"start":{"line":345,"column":28},"end":{"line":347,"column":14}},"125":{"start":{"line":349,"column":12},"end":{"line":355,"column":15}},"126":{"start":{"line":350,"column":16},"end":{"line":350,"column":39}},"127":{"start":{"line":352,"column":16},"end":{"line":354,"column":17}},"128":{"start":{"line":353,"column":20},"end":{"line":353,"column":55}},"129":{"start":{"line":358,"column":16},"end":{"line":359,"column":null}},"130":{"start":{"line":362,"column":12},"end":{"line":372,"column":15}},"131":{"start":{"line":363,"column":16},"end":{"line":371,"column":17}},"132":{"start":{"line":367,"column":20},"end":{"line":370,"column":26}},"133":{"start":{"line":374,"column":39},"end":{"line":377,"column":null}},"134":{"start":{"line":376,"column":20},"end":{"line":377,"column":54}},"135":{"start":{"line":380,"column":16},"end":{"line":380,"column":68}},"136":{"start":{"line":381,"column":35},"end":{"line":382,"column":null}},"137":{"start":{"line":382,"column":23},"end":{"line":382,"column":53}},"138":{"start":{"line":384,"column":12},"end":{"line":388,"column":13}},"139":{"start":{"line":385,"column":16},"end":{"line":387,"column":37}},"140":{"start":{"line":390,"column":12},"end":{"line":401,"column":13}},"141":{"start":{"line":391,"column":16},"end":{"line":396,"column":17}},"142":{"start":{"line":392,"column":37},"end":{"line":392,"column":78}},"143":{"start":{"line":393,"column":20},"end":{"line":395,"column":21}},"144":{"start":{"line":394,"column":24},"end":{"line":394,"column":63}},"145":{"start":{"line":397,"column":26},"end":{"line":397,"column":54}},"146":{"start":{"line":398,"column":16},"end":{"line":400,"column":17}},"147":{"start":{"line":399,"column":20},"end":{"line":399,"column":54}},"148":{"start":{"line":403,"column":12},"end":{"line":411,"column":13}},"149":{"start":{"line":404,"column":16},"end":{"line":410,"column":17}},"150":{"start":{"line":406,"column":24},"end":{"line":406,"column":69}},"151":{"start":{"line":407,"column":20},"end":{"line":409,"column":21}},"152":{"start":{"line":408,"column":24},"end":{"line":408,"column":69}},"153":{"start":{"line":413,"column":30},"end":{"line":413,"column":79}},"154":{"start":{"line":414,"column":12},"end":{"line":414,"column":72}},"155":{"start":{"line":414,"column":44},"end":{"line":414,"column":70}},"156":{"start":{"line":417,"column":35},"end":{"line":426,"column":null}},"157":{"start":{"line":419,"column":16},"end":{"line":425,"column":18}},"158":{"start":{"line":429,"column":25},"end":{"line":429,"column":76}},"159":{"start":{"line":430,"column":36},"end":{"line":433,"column":null}},"160":{"start":{"line":435,"column":33},"end":{"line":437,"column":null}},"161":{"start":{"line":440,"column":28},"end":{"line":442,"column":31}},"162":{"start":{"line":441,"column":57},"end":{"line":441,"column":62}},"163":{"start":{"line":442,"column":25},"end":{"line":442,"column":30}},"164":{"start":{"line":443,"column":33},"end":{"line":445,"column":31}},"165":{"start":{"line":444,"column":62},"end":{"line":444,"column":77}},"166":{"start":{"line":445,"column":25},"end":{"line":445,"column":30}},"167":{"start":{"line":446,"column":29},"end":{"line":446,"column":43}},"168":{"start":{"line":447,"column":8},"end":{"line":449,"column":11}},"169":{"start":{"line":448,"column":12},"end":{"line":448,"column":56}},"170":{"start":{"line":450,"column":31},"end":{"line":452,"column":31}},"171":{"start":{"line":451,"column":60},"end":{"line":451,"column":73}},"172":{"start":{"line":452,"column":25},"end":{"line":452,"column":30}},"173":{"start":{"line":453,"column":27},"end":{"line":453,"column":76}},"174":{"start":{"line":453,"column":70},"end":{"line":453,"column":75}},"175":{"start":{"line":454,"column":31},"end":{"line":455,"column":null}},"176":{"start":{"line":455,"column":22},"end":{"line":455,"column":27}},"177":{"start":{"line":457,"column":33},"end":{"line":458,"column":null}},"178":{"start":{"line":458,"column":22},"end":{"line":458,"column":27}},"179":{"start":{"line":460,"column":31},"end":{"line":461,"column":null}},"180":{"start":{"line":461,"column":22},"end":{"line":461,"column":27}},"181":{"start":{"line":464,"column":29},"end":{"line":466,"column":39}},"182":{"start":{"line":465,"column":32},"end":{"line":465,"column":56}},"183":{"start":{"line":467,"column":28},"end":{"line":469,"column":39}},"184":{"start":{"line":468,"column":32},"end":{"line":468,"column":55}},"185":{"start":{"line":470,"column":29},"end":{"line":472,"column":39}},"186":{"start":{"line":471,"column":32},"end":{"line":471,"column":56}},"187":{"start":{"line":474,"column":23},"end":{"line":506,"column":10}},"188":{"start":{"line":508,"column":30},"end":{"line":508,"column":72}},"189":{"start":{"line":510,"column":8},"end":{"line":512,"column":11}},"190":{"start":{"line":514,"column":8},"end":{"line":516,"column":10}},"191":{"start":{"line":518,"column":35},"end":{"line":520,"column":null}},"192":{"start":{"line":523,"column":8},"end":{"line":525,"column":11}},"193":{"start":{"line":527,"column":8},"end":{"line":529,"column":10}},"194":{"start":{"line":531,"column":27},"end":{"line":532,"column":null}},"195":{"start":{"line":534,"column":8},"end":{"line":538,"column":9}},"196":{"start":{"line":535,"column":12},"end":{"line":537,"column":14}},"197":{"start":{"line":540,"column":8},"end":{"line":542,"column":10}},"198":{"start":{"line":544,"column":8},"end":{"line":544,"column":26}},"199":{"start":{"line":556,"column":29},"end":{"line":558,"column":68}},"200":{"start":{"line":557,"column":50},"end":{"line":557,"column":79}},"201":{"start":{"line":558,"column":25},"end":{"line":558,"column":66}},"202":{"start":{"line":559,"column":8},"end":{"line":559,"column":51}},"203":{"start":{"line":559,"column":39},"end":{"line":559,"column":51}},"204":{"start":{"line":560,"column":20},"end":{"line":562,"column":null}},"205":{"start":{"line":561,"column":28},"end":{"line":561,"column":43}},"206":{"start":{"line":564,"column":8},"end":{"line":564,"column":50}},"207":{"start":{"line":575,"column":8},"end":{"line":575,"column":63}},"208":{"start":{"line":575,"column":51},"end":{"line":575,"column":63}},"209":{"start":{"line":577,"column":12},"end":{"line":577,"column":65}},"210":{"start":{"line":578,"column":8},"end":{"line":578,"column":47}},"211":{"start":{"line":592,"column":8},"end":{"line":594,"column":9}},"212":{"start":{"line":593,"column":12},"end":{"line":593,"column":34}},"213":{"start":{"line":596,"column":24},"end":{"line":596,"column":75}},"214":{"start":{"line":598,"column":8},"end":{"line":598,"column":60}},"215":{"start":{"line":617,"column":70},"end":{"line":617,"column":72}},"216":{"start":{"line":619,"column":8},"end":{"line":682,"column":9}},"217":{"start":{"line":620,"column":69},"end":{"line":620,"column":71}},"218":{"start":{"line":621,"column":12},"end":{"line":627,"column":15}},"219":{"start":{"line":622,"column":16},"end":{"line":626,"column":19}},"220":{"start":{"line":623,"column":20},"end":{"line":625,"column":21}},"221":{"start":{"line":624,"column":24},"end":{"line":624,"column":56}},"222":{"start":{"line":629,"column":34},"end":{"line":630,"column":null}},"223":{"start":{"line":630,"column":51},"end":{"line":630,"column":76}},"224":{"start":{"line":632,"column":34},"end":{"line":633,"column":null}},"225":{"start":{"line":633,"column":51},"end":{"line":633,"column":66}},"226":{"start":{"line":635,"column":35},"end":{"line":636,"column":null}},"227":{"start":{"line":636,"column":51},"end":{"line":636,"column":67}},"228":{"start":{"line":639,"column":37},"end":{"line":641,"column":null}},"229":{"start":{"line":643,"column":37},"end":{"line":645,"column":null}},"230":{"start":{"line":647,"column":38},"end":{"line":649,"column":null}},"231":{"start":{"line":652,"column":42},"end":{"line":654,"column":null}},"232":{"start":{"line":656,"column":43},"end":{"line":658,"column":null}},"233":{"start":{"line":661,"column":12},"end":{"line":681,"column":14}},"234":{"start":{"line":684,"column":8},"end":{"line":684,"column":35}},"235":{"start":{"line":697,"column":30},"end":{"line":698,"column":null}},"236":{"start":{"line":698,"column":45},"end":{"line":698,"column":73}},"237":{"start":{"line":700,"column":30},"end":{"line":701,"column":null}},"238":{"start":{"line":701,"column":45},"end":{"line":701,"column":63}},"239":{"start":{"line":703,"column":31},"end":{"line":704,"column":null}},"240":{"start":{"line":704,"column":45},"end":{"line":704,"column":64}},"241":{"start":{"line":707,"column":33},"end":{"line":709,"column":null}},"242":{"start":{"line":711,"column":33},"end":{"line":713,"column":null}},"243":{"start":{"line":715,"column":34},"end":{"line":717,"column":null}},"244":{"start":{"line":720,"column":38},"end":{"line":722,"column":null}},"245":{"start":{"line":724,"column":39},"end":{"line":726,"column":null}},"246":{"start":{"line":728,"column":8},"end":{"line":739,"column":10}},"247":{"start":{"line":753,"column":8},"end":{"line":755,"column":9}},"248":{"start":{"line":754,"column":12},"end":{"line":754,"column":24}},"249":{"start":{"line":756,"column":8},"end":{"line":756,"column":71}},"250":{"start":{"line":770,"column":8},"end":{"line":772,"column":9}},"251":{"start":{"line":771,"column":12},"end":{"line":771,"column":24}},"252":{"start":{"line":774,"column":8},"end":{"line":774,"column":57}},"253":{"start":{"line":783,"column":8},"end":{"line":783,"column":40}},"254":{"start":{"line":783,"column":28},"end":{"line":783,"column":40}},"255":{"start":{"line":784,"column":8},"end":{"line":784,"column":60}},"256":{"start":{"line":798,"column":53},"end":{"line":825,"column":14}},"257":{"start":{"line":800,"column":30},"end":{"line":800,"column":53}},"258":{"start":{"line":801,"column":30},"end":{"line":801,"column":53}},"259":{"start":{"line":802,"column":16},"end":{"line":802,"column":50}},"260":{"start":{"line":805,"column":16},"end":{"line":824,"column":18}},"261":{"start":{"line":827,"column":29},"end":{"line":829,"column":70}},"262":{"start":{"line":828,"column":24},"end":{"line":828,"column":39}},"263":{"start":{"line":829,"column":40},"end":{"line":829,"column":69}},"264":{"start":{"line":830,"column":28},"end":{"line":832,"column":70}},"265":{"start":{"line":831,"column":24},"end":{"line":831,"column":38}},"266":{"start":{"line":832,"column":40},"end":{"line":832,"column":69}},"267":{"start":{"line":835,"column":12},"end":{"line":835,"column":70}},"268":{"start":{"line":837,"column":12},"end":{"line":837,"column":70}},"269":{"start":{"line":839,"column":12},"end":{"line":839,"column":68}},"270":{"start":{"line":841,"column":12},"end":{"line":841,"column":68}},"271":{"start":{"line":843,"column":58},"end":{"line":868,"column":10}},"272":{"start":{"line":847,"column":26},"end":{"line":847,"column":77}},"273":{"start":{"line":853,"column":26},"end":{"line":853,"column":77}},"274":{"start":{"line":859,"column":26},"end":{"line":859,"column":75}},"275":{"start":{"line":865,"column":26},"end":{"line":865,"column":75}},"276":{"start":{"line":870,"column":57},"end":{"line":870,"column":59}},"277":{"start":{"line":872,"column":8},"end":{"line":884,"column":10}},"278":{"start":{"line":886,"column":8},"end":{"line":898,"column":10}},"279":{"start":{"line":900,"column":8},"end":{"line":912,"column":10}},"280":{"start":{"line":914,"column":8},"end":{"line":926,"column":10}},"281":{"start":{"line":928,"column":8},"end":{"line":928,"column":24}},"282":{"start":{"line":936,"column":30},"end":{"line":942,"column":5}},"283":{"start":{"line":937,"column":28},"end":{"line":938,"column":null}},"284":{"start":{"line":938,"column":37},"end":{"line":938,"column":70}},"285":{"start":{"line":940,"column":8},"end":{"line":940,"column":50}},"286":{"start":{"line":940,"column":38},"end":{"line":940,"column":50}},"287":{"start":{"line":941,"column":8},"end":{"line":941,"column":64}},"288":{"start":{"line":950,"column":18},"end":{"line":950,"column":28}},"289":{"start":{"line":951,"column":8},"end":{"line":953,"column":9}},"290":{"start":{"line":952,"column":12},"end":{"line":952,"column":34}},"291":{"start":{"line":954,"column":8},"end":{"line":954,"column":39}},"292":{"start":{"line":95,"column":13},"end":{"line":95,"column":38}},"293":{"start":{"line":95,"column":13},"end":{"line":956,"column":null}}},"fnMap":{"0":{"name":"(anonymous_5)","decl":{"start":{"line":98,"column":4},"end":{"line":98,"column":null}},"loc":{"start":{"line":128,"column":75},"end":{"line":129,"column":8}}},"1":{"name":"(anonymous_6)","decl":{"start":{"line":131,"column":4},"end":{"line":131,"column":9}},"loc":{"start":{"line":133,"column":26},"end":{"line":137,"column":5}}},"2":{"name":"(anonymous_7)","decl":{"start":{"line":143,"column":4},"end":{"line":143,"column":9}},"loc":{"start":{"line":143,"column":61},"end":{"line":155,"column":5}}},"3":{"name":"(anonymous_8)","decl":{"start":{"line":157,"column":4},"end":{"line":157,"column":9}},"loc":{"start":{"line":159,"column":26},"end":{"line":172,"column":5}}},"4":{"name":"(anonymous_9)","decl":{"start":{"line":179,"column":4},"end":{"line":179,"column":9}},"loc":{"start":{"line":181,"column":26},"end":{"line":207,"column":5}}},"5":{"name":"(anonymous_10)","decl":{"start":{"line":199,"column":12},"end":{"line":199,"column":13}},"loc":{"start":{"line":199,"column":24},"end":{"line":199,"column":53}}},"6":{"name":"(anonymous_11)","decl":{"start":{"line":209,"column":4},"end":{"line":209,"column":9}},"loc":{"start":{"line":211,"column":26},"end":{"line":223,"column":5}}},"7":{"name":"(anonymous_12)","decl":{"start":{"line":230,"column":4},"end":{"line":230,"column":9}},"loc":{"start":{"line":231,"column":23},"end":{"line":545,"column":5}}},"8":{"name":"(anonymous_13)","decl":{"start":{"line":319,"column":32},"end":{"line":319,"column":33}},"loc":{"start":{"line":319,"column":47},"end":{"line":325,"column":13}}},"9":{"name":"(anonymous_14)","decl":{"start":{"line":349,"column":28},"end":{"line":349,"column":29}},"loc":{"start":{"line":349,"column":39},"end":{"line":355,"column":13}}},"10":{"name":"(anonymous_15)","decl":{"start":{"line":362,"column":34},"end":{"line":362,"column":35}},"loc":{"start":{"line":362,"column":43},"end":{"line":372,"column":13}}},"11":{"name":"(anonymous_16)","decl":{"start":{"line":375,"column":16},"end":{"line":375,"column":17}},"loc":{"start":{"line":376,"column":20},"end":{"line":377,"column":54}}},"12":{"name":"(anonymous_17)","decl":{"start":{"line":382,"column":16},"end":{"line":382,"column":17}},"loc":{"start":{"line":382,"column":23},"end":{"line":382,"column":53}}},"13":{"name":"(anonymous_18)","decl":{"start":{"line":414,"column":30},"end":{"line":414,"column":31}},"loc":{"start":{"line":414,"column":44},"end":{"line":414,"column":70}}},"14":{"name":"(anonymous_19)","decl":{"start":{"line":418,"column":24},"end":{"line":418,"column":29}},"loc":{"start":{"line":418,"column":41},"end":{"line":426,"column":13}}},"15":{"name":"(anonymous_20)","decl":{"start":{"line":441,"column":50},"end":{"line":441,"column":51}},"loc":{"start":{"line":441,"column":57},"end":{"line":441,"column":62}}},"16":{"name":"(anonymous_21)","decl":{"start":{"line":442,"column":15},"end":{"line":442,"column":16}},"loc":{"start":{"line":442,"column":25},"end":{"line":442,"column":30}}},"17":{"name":"(anonymous_22)","decl":{"start":{"line":444,"column":55},"end":{"line":444,"column":56}},"loc":{"start":{"line":444,"column":62},"end":{"line":444,"column":77}}},"18":{"name":"(anonymous_23)","decl":{"start":{"line":445,"column":15},"end":{"line":445,"column":16}},"loc":{"start":{"line":445,"column":25},"end":{"line":445,"column":30}}},"19":{"name":"(anonymous_24)","decl":{"start":{"line":447,"column":55},"end":{"line":447,"column":56}},"loc":{"start":{"line":447,"column":65},"end":{"line":449,"column":9}}},"20":{"name":"(anonymous_25)","decl":{"start":{"line":451,"column":53},"end":{"line":451,"column":54}},"loc":{"start":{"line":451,"column":60},"end":{"line":451,"column":73}}},"21":{"name":"(anonymous_26)","decl":{"start":{"line":452,"column":15},"end":{"line":452,"column":16}},"loc":{"start":{"line":452,"column":25},"end":{"line":452,"column":30}}},"22":{"name":"(anonymous_27)","decl":{"start":{"line":453,"column":60},"end":{"line":453,"column":61}},"loc":{"start":{"line":453,"column":70},"end":{"line":453,"column":75}}},"23":{"name":"(anonymous_28)","decl":{"start":{"line":455,"column":12},"end":{"line":455,"column":13}},"loc":{"start":{"line":455,"column":22},"end":{"line":455,"column":27}}},"24":{"name":"(anonymous_29)","decl":{"start":{"line":458,"column":12},"end":{"line":458,"column":13}},"loc":{"start":{"line":458,"column":22},"end":{"line":458,"column":27}}},"25":{"name":"(anonymous_30)","decl":{"start":{"line":461,"column":12},"end":{"line":461,"column":13}},"loc":{"start":{"line":461,"column":22},"end":{"line":461,"column":27}}},"26":{"name":"(anonymous_31)","decl":{"start":{"line":465,"column":20},"end":{"line":465,"column":21}},"loc":{"start":{"line":465,"column":32},"end":{"line":465,"column":56}}},"27":{"name":"(anonymous_32)","decl":{"start":{"line":468,"column":20},"end":{"line":468,"column":21}},"loc":{"start":{"line":468,"column":32},"end":{"line":468,"column":55}}},"28":{"name":"(anonymous_33)","decl":{"start":{"line":471,"column":20},"end":{"line":471,"column":21}},"loc":{"start":{"line":471,"column":32},"end":{"line":471,"column":56}}},"29":{"name":"(anonymous_34)","decl":{"start":{"line":553,"column":12},"end":{"line":553,"column":28}},"loc":{"start":{"line":554,"column":56},"end":{"line":565,"column":5}}},"30":{"name":"(anonymous_35)","decl":{"start":{"line":557,"column":20},"end":{"line":557,"column":21}},"loc":{"start":{"line":557,"column":50},"end":{"line":557,"column":79}}},"31":{"name":"(anonymous_36)","decl":{"start":{"line":558,"column":17},"end":{"line":558,"column":18}},"loc":{"start":{"line":558,"column":25},"end":{"line":558,"column":66}}},"32":{"name":"(anonymous_37)","decl":{"start":{"line":561,"column":12},"end":{"line":561,"column":13}},"loc":{"start":{"line":561,"column":28},"end":{"line":561,"column":43}}},"33":{"name":"(anonymous_38)","decl":{"start":{"line":572,"column":12},"end":{"line":572,"column":24}},"loc":{"start":{"line":573,"column":50},"end":{"line":579,"column":5}}},"34":{"name":"(anonymous_39)","decl":{"start":{"line":588,"column":12},"end":{"line":588,"column":28}},"loc":{"start":{"line":590,"column":30},"end":{"line":599,"column":5}}},"35":{"name":"(anonymous_40)","decl":{"start":{"line":609,"column":12},"end":{"line":609,"column":41}},"loc":{"start":{"line":615,"column":42},"end":{"line":685,"column":5}}},"36":{"name":"(anonymous_41)","decl":{"start":{"line":621,"column":36},"end":{"line":621,"column":37}},"loc":{"start":{"line":621,"column":42},"end":{"line":627,"column":13}}},"37":{"name":"(anonymous_42)","decl":{"start":{"line":622,"column":43},"end":{"line":622,"column":44}},"loc":{"start":{"line":622,"column":49},"end":{"line":626,"column":17}}},"38":{"name":"(anonymous_43)","decl":{"start":{"line":630,"column":44},"end":{"line":630,"column":45}},"loc":{"start":{"line":630,"column":51},"end":{"line":630,"column":76}}},"39":{"name":"(anonymous_44)","decl":{"start":{"line":633,"column":44},"end":{"line":633,"column":45}},"loc":{"start":{"line":633,"column":51},"end":{"line":633,"column":66}}},"40":{"name":"(anonymous_45)","decl":{"start":{"line":636,"column":44},"end":{"line":636,"column":45}},"loc":{"start":{"line":636,"column":51},"end":{"line":636,"column":67}}},"41":{"name":"(anonymous_46)","decl":{"start":{"line":693,"column":12},"end":{"line":693,"column":44}},"loc":{"start":{"line":695,"column":25},"end":{"line":740,"column":5}}},"42":{"name":"(anonymous_47)","decl":{"start":{"line":698,"column":35},"end":{"line":698,"column":36}},"loc":{"start":{"line":698,"column":45},"end":{"line":698,"column":73}}},"43":{"name":"(anonymous_48)","decl":{"start":{"line":701,"column":35},"end":{"line":701,"column":36}},"loc":{"start":{"line":701,"column":45},"end":{"line":701,"column":63}}},"44":{"name":"(anonymous_49)","decl":{"start":{"line":704,"column":35},"end":{"line":704,"column":36}},"loc":{"start":{"line":704,"column":45},"end":{"line":704,"column":64}}},"45":{"name":"(anonymous_50)","decl":{"start":{"line":749,"column":12},"end":{"line":749,"column":31}},"loc":{"start":{"line":751,"column":25},"end":{"line":757,"column":5}}},"46":{"name":"(anonymous_51)","decl":{"start":{"line":766,"column":12},"end":{"line":766,"column":26}},"loc":{"start":{"line":768,"column":28},"end":{"line":775,"column":5}}},"47":{"name":"(anonymous_52)","decl":{"start":{"line":782,"column":12},"end":{"line":782,"column":27}},"loc":{"start":{"line":782,"column":49},"end":{"line":785,"column":5}}},"48":{"name":"(anonymous_53)","decl":{"start":{"line":794,"column":12},"end":{"line":794,"column":35}},"loc":{"start":{"line":796,"column":33},"end":{"line":929,"column":5}}},"49":{"name":"(anonymous_54)","decl":{"start":{"line":799,"column":18},"end":{"line":799,"column":19}},"loc":{"start":{"line":799,"column":27},"end":{"line":803,"column":13}}},"50":{"name":"(anonymous_55)","decl":{"start":{"line":804,"column":17},"end":{"line":804,"column":18}},"loc":{"start":{"line":804,"column":35},"end":{"line":825,"column":13}}},"51":{"name":"(anonymous_56)","decl":{"start":{"line":828,"column":17},"end":{"line":828,"column":18}},"loc":{"start":{"line":828,"column":24},"end":{"line":828,"column":39}}},"52":{"name":"(anonymous_57)","decl":{"start":{"line":829,"column":20},"end":{"line":829,"column":21}},"loc":{"start":{"line":829,"column":40},"end":{"line":829,"column":69}}},"53":{"name":"(anonymous_58)","decl":{"start":{"line":831,"column":17},"end":{"line":831,"column":18}},"loc":{"start":{"line":831,"column":24},"end":{"line":831,"column":38}}},"54":{"name":"(anonymous_59)","decl":{"start":{"line":832,"column":20},"end":{"line":832,"column":21}},"loc":{"start":{"line":832,"column":40},"end":{"line":832,"column":69}}},"55":{"name":"(anonymous_60)","decl":{"start":{"line":846,"column":36},"end":{"line":846,"column":37}},"loc":{"start":{"line":847,"column":26},"end":{"line":847,"column":77}}},"56":{"name":"(anonymous_61)","decl":{"start":{"line":852,"column":36},"end":{"line":852,"column":37}},"loc":{"start":{"line":853,"column":26},"end":{"line":853,"column":77}}},"57":{"name":"(anonymous_62)","decl":{"start":{"line":858,"column":36},"end":{"line":858,"column":37}},"loc":{"start":{"line":859,"column":26},"end":{"line":859,"column":75}}},"58":{"name":"(anonymous_63)","decl":{"start":{"line":864,"column":36},"end":{"line":864,"column":37}},"loc":{"start":{"line":865,"column":26},"end":{"line":865,"column":75}}},"59":{"name":"(anonymous_64)","decl":{"start":{"line":936,"column":30},"end":{"line":936,"column":31}},"loc":{"start":{"line":936,"column":70},"end":{"line":942,"column":5}}},"60":{"name":"(anonymous_65)","decl":{"start":{"line":938,"column":12},"end":{"line":938,"column":13}},"loc":{"start":{"line":938,"column":37},"end":{"line":938,"column":70}}},"61":{"name":"(anonymous_66)","decl":{"start":{"line":949,"column":12},"end":{"line":949,"column":42}},"loc":{"start":{"line":949,"column":61},"end":{"line":955,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":144,"column":8},"end":{"line":144,"column":27}},"type":"if","locations":[{"start":{"line":144,"column":8},"end":{"line":144,"column":27}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":147,"column":12},"end":{"line":148,"column":51}},"type":"binary-expr","locations":[{"start":{"line":147,"column":12},"end":{"line":147,"column":54}},{"start":{"line":148,"column":12},"end":{"line":148,"column":51}}]},"2":{"loc":{"start":{"line":150,"column":8},"end":{"line":154,"column":9}},"type":"if","locations":[{"start":{"line":150,"column":8},"end":{"line":154,"column":9}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":163,"column":8},"end":{"line":167,"column":9}},"type":"if","locations":[{"start":{"line":163,"column":8},"end":{"line":167,"column":9}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":183,"column":8},"end":{"line":183,"column":27}},"type":"if","locations":[{"start":{"line":183,"column":8},"end":{"line":183,"column":27}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":186,"column":12},"end":{"line":187,"column":51}},"type":"binary-expr","locations":[{"start":{"line":186,"column":12},"end":{"line":186,"column":54}},{"start":{"line":187,"column":12},"end":{"line":187,"column":51}}]},"6":{"loc":{"start":{"line":189,"column":8},"end":{"line":189,"column":32}},"type":"if","locations":[{"start":{"line":189,"column":8},"end":{"line":189,"column":32}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":193,"column":8},"end":{"line":196,"column":9}},"type":"if","locations":[{"start":{"line":193,"column":8},"end":{"line":196,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":193,"column":12},"end":{"line":193,"column":44}},"type":"binary-expr","locations":[{"start":{"line":193,"column":12},"end":{"line":193,"column":20}},{"start":{"line":193,"column":24},"end":{"line":193,"column":44}}]},"9":{"loc":{"start":{"line":202,"column":8},"end":{"line":202,"column":38}},"type":"if","locations":[{"start":{"line":202,"column":8},"end":{"line":202,"column":38}},{"start":{},"end":{}}]},"10":{"loc":{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},"type":"if","locations":[{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":236,"column":8},"end":{"line":238,"column":9}},"type":"if","locations":[{"start":{"line":236,"column":8},"end":{"line":238,"column":9}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":241,"column":8},"end":{"line":244,"column":9}},"type":"if","locations":[{"start":{"line":241,"column":8},"end":{"line":244,"column":9}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":247,"column":8},"end":{"line":251,"column":9}},"type":"if","locations":[{"start":{"line":247,"column":8},"end":{"line":251,"column":9}},{"start":{},"end":{}}]},"14":{"loc":{"start":{"line":247,"column":12},"end":{"line":247,"column":44}},"type":"binary-expr","locations":[{"start":{"line":247,"column":12},"end":{"line":247,"column":20}},{"start":{"line":247,"column":24},"end":{"line":247,"column":44}}]},"15":{"loc":{"start":{"line":265,"column":12},"end":{"line":268,"column":13}},"type":"if","locations":[{"start":{"line":265,"column":12},"end":{"line":268,"column":13}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":273,"column":29},"end":{"line":275,"column":36}},"type":"cond-expr","locations":[{"start":{"line":274,"column":22},"end":{"line":274,"column":58}},{"start":{"line":275,"column":22},"end":{"line":275,"column":36}}]},"17":{"loc":{"start":{"line":276,"column":30},"end":{"line":278,"column":36}},"type":"cond-expr","locations":[{"start":{"line":277,"column":22},"end":{"line":277,"column":59}},{"start":{"line":278,"column":22},"end":{"line":278,"column":36}}]},"18":{"loc":{"start":{"line":303,"column":12},"end":{"line":313,"column":13}},"type":"if","locations":[{"start":{"line":303,"column":12},"end":{"line":313,"column":13}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":306,"column":16},"end":{"line":308,"column":17}},"type":"if","locations":[{"start":{"line":306,"column":16},"end":{"line":308,"column":17}},{"start":{},"end":{}}]},"20":{"loc":{"start":{"line":310,"column":16},"end":{"line":312,"column":17}},"type":"if","locations":[{"start":{"line":310,"column":16},"end":{"line":312,"column":17}},{"start":{},"end":{}}]},"21":{"loc":{"start":{"line":322,"column":16},"end":{"line":324,"column":17}},"type":"if","locations":[{"start":{"line":322,"column":16},"end":{"line":324,"column":17}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":328,"column":16},"end":{"line":333,"column":17}},"type":"if","locations":[{"start":{"line":328,"column":16},"end":{"line":333,"column":17}},{"start":{},"end":{}}]},"23":{"loc":{"start":{"line":330,"column":20},"end":{"line":332,"column":21}},"type":"if","locations":[{"start":{"line":330,"column":20},"end":{"line":332,"column":21}},{"start":{},"end":{}}]},"24":{"loc":{"start":{"line":337,"column":16},"end":{"line":342,"column":17}},"type":"if","locations":[{"start":{"line":337,"column":16},"end":{"line":342,"column":17}},{"start":{},"end":{}}]},"25":{"loc":{"start":{"line":339,"column":20},"end":{"line":341,"column":21}},"type":"if","locations":[{"start":{"line":339,"column":20},"end":{"line":341,"column":21}},{"start":{},"end":{}}]},"26":{"loc":{"start":{"line":352,"column":16},"end":{"line":354,"column":17}},"type":"if","locations":[{"start":{"line":352,"column":16},"end":{"line":354,"column":17}},{"start":{},"end":{}}]},"27":{"loc":{"start":{"line":363,"column":16},"end":{"line":371,"column":17}},"type":"if","locations":[{"start":{"line":363,"column":16},"end":{"line":371,"column":17}},{"start":{},"end":{}}]},"28":{"loc":{"start":{"line":380,"column":16},"end":{"line":380,"column":68}},"type":"binary-expr","locations":[{"start":{"line":380,"column":16},"end":{"line":380,"column":34}},{"start":{"line":380,"column":38},"end":{"line":380,"column":68}}]},"29":{"loc":{"start":{"line":384,"column":12},"end":{"line":388,"column":13}},"type":"if","locations":[{"start":{"line":384,"column":12},"end":{"line":388,"column":13}},{"start":{},"end":{}}]},"30":{"loc":{"start":{"line":385,"column":46},"end":{"line":387,"column":36}},"type":"cond-expr","locations":[{"start":{"line":386,"column":22},"end":{"line":386,"column":38}},{"start":{"line":387,"column":22},"end":{"line":387,"column":36}}]},"31":{"loc":{"start":{"line":391,"column":16},"end":{"line":396,"column":17}},"type":"if","locations":[{"start":{"line":391,"column":16},"end":{"line":396,"column":17}},{"start":{},"end":{}}]},"32":{"loc":{"start":{"line":393,"column":20},"end":{"line":395,"column":21}},"type":"if","locations":[{"start":{"line":393,"column":20},"end":{"line":395,"column":21}},{"start":{},"end":{}}]},"33":{"loc":{"start":{"line":398,"column":16},"end":{"line":400,"column":17}},"type":"if","locations":[{"start":{"line":398,"column":16},"end":{"line":400,"column":17}},{"start":{},"end":{}}]},"34":{"loc":{"start":{"line":398,"column":20},"end":{"line":398,"column":39}},"type":"binary-expr","locations":[{"start":{"line":398,"column":20},"end":{"line":398,"column":21}},{"start":{"line":398,"column":25},"end":{"line":398,"column":39}}]},"35":{"loc":{"start":{"line":404,"column":16},"end":{"line":410,"column":17}},"type":"if","locations":[{"start":{"line":404,"column":16},"end":{"line":410,"column":17}},{"start":{},"end":{}}]},"36":{"loc":{"start":{"line":407,"column":20},"end":{"line":409,"column":21}},"type":"if","locations":[{"start":{"line":407,"column":20},"end":{"line":409,"column":21}},{"start":{},"end":{}}]},"37":{"loc":{"start":{"line":534,"column":8},"end":{"line":538,"column":9}},"type":"if","locations":[{"start":{"line":534,"column":8},"end":{"line":538,"column":9}},{"start":{},"end":{}}]},"38":{"loc":{"start":{"line":557,"column":50},"end":{"line":557,"column":79}},"type":"binary-expr","locations":[{"start":{"line":557,"column":50},"end":{"line":557,"column":60}},{"start":{"line":557,"column":64},"end":{"line":557,"column":79}}]},"39":{"loc":{"start":{"line":558,"column":25},"end":{"line":558,"column":66}},"type":"cond-expr","locations":[{"start":{"line":558,"column":48},"end":{"line":558,"column":49}},{"start":{"line":558,"column":52},"end":{"line":558,"column":66}}]},"40":{"loc":{"start":{"line":559,"column":8},"end":{"line":559,"column":51}},"type":"if","locations":[{"start":{"line":559,"column":8},"end":{"line":559,"column":51}},{"start":{},"end":{}}]},"41":{"loc":{"start":{"line":575,"column":8},"end":{"line":575,"column":63}},"type":"if","locations":[{"start":{"line":575,"column":8},"end":{"line":575,"column":63}},{"start":{},"end":{}}]},"42":{"loc":{"start":{"line":575,"column":12},"end":{"line":575,"column":49}},"type":"binary-expr","locations":[{"start":{"line":575,"column":12},"end":{"line":575,"column":26}},{"start":{"line":575,"column":30},"end":{"line":575,"column":49}}]},"43":{"loc":{"start":{"line":577,"column":12},"end":{"line":577,"column":65}},"type":"cond-expr","locations":[{"start":{"line":577,"column":39},"end":{"line":577,"column":44}},{"start":{"line":577,"column":47},"end":{"line":577,"column":65}}]},"44":{"loc":{"start":{"line":592,"column":8},"end":{"line":594,"column":9}},"type":"if","locations":[{"start":{"line":592,"column":8},"end":{"line":594,"column":9}},{"start":{},"end":{}}]},"45":{"loc":{"start":{"line":598,"column":15},"end":{"line":598,"column":59}},"type":"binary-expr","locations":[{"start":{"line":598,"column":15},"end":{"line":598,"column":41}},{"start":{"line":598,"column":45},"end":{"line":598,"column":59}}]},"46":{"loc":{"start":{"line":623,"column":20},"end":{"line":625,"column":21}},"type":"if","locations":[{"start":{"line":623,"column":20},"end":{"line":625,"column":21}},{"start":{},"end":{}}]},"47":{"loc":{"start":{"line":667,"column":24},"end":{"line":667,"column":64}},"type":"binary-expr","locations":[{"start":{"line":667,"column":24},"end":{"line":667,"column":56}},{"start":{"line":667,"column":60},"end":{"line":667,"column":64}}]},"48":{"loc":{"start":{"line":668,"column":35},"end":{"line":668,"column":75}},"type":"binary-expr","locations":[{"start":{"line":668,"column":35},"end":{"line":668,"column":67}},{"start":{"line":668,"column":71},"end":{"line":668,"column":75}}]},"49":{"loc":{"start":{"line":669,"column":36},"end":{"line":669,"column":77}},"type":"binary-expr","locations":[{"start":{"line":669,"column":36},"end":{"line":669,"column":69}},{"start":{"line":669,"column":73},"end":{"line":669,"column":77}}]},"50":{"loc":{"start":{"line":671,"column":24},"end":{"line":671,"column":67}},"type":"binary-expr","locations":[{"start":{"line":671,"column":24},"end":{"line":671,"column":59}},{"start":{"line":671,"column":63},"end":{"line":671,"column":67}}]},"51":{"loc":{"start":{"line":673,"column":24},"end":{"line":673,"column":67}},"type":"binary-expr","locations":[{"start":{"line":673,"column":24},"end":{"line":673,"column":59}},{"start":{"line":673,"column":63},"end":{"line":673,"column":67}}]},"52":{"loc":{"start":{"line":675,"column":24},"end":{"line":675,"column":68}},"type":"binary-expr","locations":[{"start":{"line":675,"column":24},"end":{"line":675,"column":60}},{"start":{"line":675,"column":64},"end":{"line":675,"column":68}}]},"53":{"loc":{"start":{"line":677,"column":24},"end":{"line":677,"column":72}},"type":"binary-expr","locations":[{"start":{"line":677,"column":24},"end":{"line":677,"column":64}},{"start":{"line":677,"column":68},"end":{"line":677,"column":72}}]},"54":{"loc":{"start":{"line":679,"column":24},"end":{"line":679,"column":73}},"type":"binary-expr","locations":[{"start":{"line":679,"column":24},"end":{"line":679,"column":65}},{"start":{"line":679,"column":69},"end":{"line":679,"column":73}}]},"55":{"loc":{"start":{"line":753,"column":8},"end":{"line":755,"column":9}},"type":"if","locations":[{"start":{"line":753,"column":8},"end":{"line":755,"column":9}},{"start":{},"end":{}}]},"56":{"loc":{"start":{"line":753,"column":12},"end":{"line":753,"column":47}},"type":"binary-expr","locations":[{"start":{"line":753,"column":12},"end":{"line":753,"column":26}},{"start":{"line":753,"column":30},"end":{"line":753,"column":47}}]},"57":{"loc":{"start":{"line":770,"column":8},"end":{"line":772,"column":9}},"type":"if","locations":[{"start":{"line":770,"column":8},"end":{"line":772,"column":9}},{"start":{},"end":{}}]},"58":{"loc":{"start":{"line":770,"column":12},"end":{"line":770,"column":48}},"type":"binary-expr","locations":[{"start":{"line":770,"column":12},"end":{"line":770,"column":31}},{"start":{"line":770,"column":35},"end":{"line":770,"column":48}}]},"59":{"loc":{"start":{"line":783,"column":8},"end":{"line":783,"column":40}},"type":"if","locations":[{"start":{"line":783,"column":8},"end":{"line":783,"column":40}},{"start":{},"end":{}}]},"60":{"loc":{"start":{"line":800,"column":30},"end":{"line":800,"column":53}},"type":"binary-expr","locations":[{"start":{"line":800,"column":30},"end":{"line":800,"column":47}},{"start":{"line":800,"column":51},"end":{"line":800,"column":53}}]},"61":{"loc":{"start":{"line":801,"column":30},"end":{"line":801,"column":53}},"type":"binary-expr","locations":[{"start":{"line":801,"column":30},"end":{"line":801,"column":47}},{"start":{"line":801,"column":51},"end":{"line":801,"column":53}}]},"62":{"loc":{"start":{"line":829,"column":40},"end":{"line":829,"column":69}},"type":"binary-expr","locations":[{"start":{"line":829,"column":40},"end":{"line":829,"column":55}},{"start":{"line":829,"column":59},"end":{"line":829,"column":69}}]},"63":{"loc":{"start":{"line":832,"column":40},"end":{"line":832,"column":69}},"type":"binary-expr","locations":[{"start":{"line":832,"column":40},"end":{"line":832,"column":55}},{"start":{"line":832,"column":59},"end":{"line":832,"column":69}}]},"64":{"loc":{"start":{"line":835,"column":12},"end":{"line":835,"column":70}},"type":"cond-expr","locations":[{"start":{"line":835,"column":38},"end":{"line":835,"column":63}},{"start":{"line":835,"column":66},"end":{"line":835,"column":70}}]},"65":{"loc":{"start":{"line":837,"column":12},"end":{"line":837,"column":70}},"type":"cond-expr","locations":[{"start":{"line":837,"column":38},"end":{"line":837,"column":63}},{"start":{"line":837,"column":66},"end":{"line":837,"column":70}}]},"66":{"loc":{"start":{"line":839,"column":12},"end":{"line":839,"column":68}},"type":"cond-expr","locations":[{"start":{"line":839,"column":37},"end":{"line":839,"column":61}},{"start":{"line":839,"column":64},"end":{"line":839,"column":68}}]},"67":{"loc":{"start":{"line":841,"column":12},"end":{"line":841,"column":68}},"type":"cond-expr","locations":[{"start":{"line":841,"column":37},"end":{"line":841,"column":61}},{"start":{"line":841,"column":64},"end":{"line":841,"column":68}}]},"68":{"loc":{"start":{"line":845,"column":16},"end":{"line":849,"column":31}},"type":"cond-expr","locations":[{"start":{"line":846,"column":22},"end":{"line":847,"column":null}},{"start":{"line":849,"column":22},"end":{"line":849,"column":31}}]},"69":{"loc":{"start":{"line":847,"column":38},"end":{"line":847,"column":58}},"type":"binary-expr","locations":[{"start":{"line":847,"column":38},"end":{"line":847,"column":53}},{"start":{"line":847,"column":57},"end":{"line":847,"column":58}}]},"70":{"loc":{"start":{"line":851,"column":16},"end":{"line":855,"column":31}},"type":"cond-expr","locations":[{"start":{"line":852,"column":22},"end":{"line":853,"column":null}},{"start":{"line":855,"column":22},"end":{"line":855,"column":31}}]},"71":{"loc":{"start":{"line":853,"column":38},"end":{"line":853,"column":58}},"type":"binary-expr","locations":[{"start":{"line":853,"column":38},"end":{"line":853,"column":53}},{"start":{"line":853,"column":57},"end":{"line":853,"column":58}}]},"72":{"loc":{"start":{"line":857,"column":16},"end":{"line":861,"column":31}},"type":"cond-expr","locations":[{"start":{"line":858,"column":22},"end":{"line":859,"column":null}},{"start":{"line":861,"column":22},"end":{"line":861,"column":31}}]},"73":{"loc":{"start":{"line":859,"column":38},"end":{"line":859,"column":57}},"type":"binary-expr","locations":[{"start":{"line":859,"column":38},"end":{"line":859,"column":52}},{"start":{"line":859,"column":56},"end":{"line":859,"column":57}}]},"74":{"loc":{"start":{"line":863,"column":16},"end":{"line":867,"column":31}},"type":"cond-expr","locations":[{"start":{"line":864,"column":22},"end":{"line":865,"column":null}},{"start":{"line":867,"column":22},"end":{"line":867,"column":31}}]},"75":{"loc":{"start":{"line":865,"column":38},"end":{"line":865,"column":57}},"type":"binary-expr","locations":[{"start":{"line":865,"column":38},"end":{"line":865,"column":52}},{"start":{"line":865,"column":56},"end":{"line":865,"column":57}}]},"76":{"loc":{"start":{"line":876,"column":30},"end":{"line":876,"column":76}},"type":"binary-expr","locations":[{"start":{"line":876,"column":30},"end":{"line":876,"column":70}},{"start":{"line":876,"column":74},"end":{"line":876,"column":76}}]},"77":{"loc":{"start":{"line":878,"column":20},"end":{"line":878,"column":69}},"type":"binary-expr","locations":[{"start":{"line":878,"column":20},"end":{"line":878,"column":63}},{"start":{"line":878,"column":67},"end":{"line":878,"column":69}}]},"78":{"loc":{"start":{"line":890,"column":30},"end":{"line":890,"column":75}},"type":"binary-expr","locations":[{"start":{"line":890,"column":30},"end":{"line":890,"column":69}},{"start":{"line":890,"column":73},"end":{"line":890,"column":75}}]},"79":{"loc":{"start":{"line":892,"column":20},"end":{"line":892,"column":68}},"type":"binary-expr","locations":[{"start":{"line":892,"column":20},"end":{"line":892,"column":62}},{"start":{"line":892,"column":66},"end":{"line":892,"column":68}}]},"80":{"loc":{"start":{"line":904,"column":30},"end":{"line":904,"column":75}},"type":"binary-expr","locations":[{"start":{"line":904,"column":30},"end":{"line":904,"column":69}},{"start":{"line":904,"column":73},"end":{"line":904,"column":75}}]},"81":{"loc":{"start":{"line":906,"column":20},"end":{"line":906,"column":68}},"type":"binary-expr","locations":[{"start":{"line":906,"column":20},"end":{"line":906,"column":62}},{"start":{"line":906,"column":66},"end":{"line":906,"column":68}}]},"82":{"loc":{"start":{"line":918,"column":30},"end":{"line":918,"column":74}},"type":"binary-expr","locations":[{"start":{"line":918,"column":30},"end":{"line":918,"column":68}},{"start":{"line":918,"column":72},"end":{"line":918,"column":74}}]},"83":{"loc":{"start":{"line":920,"column":20},"end":{"line":920,"column":67}},"type":"binary-expr","locations":[{"start":{"line":920,"column":20},"end":{"line":920,"column":61}},{"start":{"line":920,"column":65},"end":{"line":920,"column":67}}]},"84":{"loc":{"start":{"line":938,"column":37},"end":{"line":938,"column":70}},"type":"binary-expr","locations":[{"start":{"line":938,"column":37},"end":{"line":938,"column":49}},{"start":{"line":938,"column":53},"end":{"line":938,"column":70}}]},"85":{"loc":{"start":{"line":940,"column":8},"end":{"line":940,"column":50}},"type":"if","locations":[{"start":{"line":940,"column":8},"end":{"line":940,"column":50}},{"start":{},"end":{}}]},"86":{"loc":{"start":{"line":100,"column":43},"end":{"line":100,"column":null}},"type":"cond-expr","locations":[{"start":{"line":100,"column":72},"end":{"line":100,"column":null}},{"start":{"line":100,"column":72},"end":{"line":100,"column":null}}]},"87":{"loc":{"start":{"line":100,"column":43},"end":{"line":100,"column":72}},"type":"binary-expr","locations":[{"start":{"line":100,"column":43},"end":{"line":100,"column":null}},{"start":{"line":100,"column":43},"end":{"line":100,"column":72}}]},"88":{"loc":{"start":{"line":100,"column":72},"end":{"line":102,"column":null}},"type":"cond-expr","locations":[{"start":{"line":102,"column":87},"end":{"line":102,"column":null}},{"start":{"line":102,"column":87},"end":{"line":102,"column":null}}]},"89":{"loc":{"start":{"line":100,"column":72},"end":{"line":102,"column":87}},"type":"binary-expr","locations":[{"start":{"line":100,"column":72},"end":{"line":102,"column":null}},{"start":{"line":102,"column":49},"end":{"line":102,"column":87}}]},"90":{"loc":{"start":{"line":102,"column":87},"end":{"line":104,"column":null}},"type":"cond-expr","locations":[{"start":{"line":104,"column":62},"end":{"line":104,"column":null}},{"start":{"line":104,"column":62},"end":{"line":104,"column":null}}]},"91":{"loc":{"start":{"line":102,"column":87},"end":{"line":104,"column":62}},"type":"binary-expr","locations":[{"start":{"line":102,"column":87},"end":{"line":104,"column":null}},{"start":{"line":104,"column":38},"end":{"line":104,"column":62}}]},"92":{"loc":{"start":{"line":104,"column":62},"end":{"line":106,"column":null}},"type":"cond-expr","locations":[{"start":{"line":106,"column":58},"end":{"line":106,"column":null}},{"start":{"line":106,"column":58},"end":{"line":106,"column":null}}]},"93":{"loc":{"start":{"line":104,"column":62},"end":{"line":106,"column":58}},"type":"binary-expr","locations":[{"start":{"line":104,"column":62},"end":{"line":106,"column":null}},{"start":{"line":106,"column":36},"end":{"line":106,"column":58}}]},"94":{"loc":{"start":{"line":106,"column":58},"end":{"line":108,"column":null}},"type":"cond-expr","locations":[{"start":{"line":108,"column":54},"end":{"line":108,"column":null}},{"start":{"line":108,"column":54},"end":{"line":108,"column":null}}]},"95":{"loc":{"start":{"line":106,"column":58},"end":{"line":108,"column":54}},"type":"binary-expr","locations":[{"start":{"line":106,"column":58},"end":{"line":108,"column":null}},{"start":{"line":108,"column":34},"end":{"line":108,"column":54}}]},"96":{"loc":{"start":{"line":108,"column":54},"end":{"line":110,"column":null}},"type":"cond-expr","locations":[{"start":{"line":110,"column":54},"end":{"line":110,"column":null}},{"start":{"line":110,"column":54},"end":{"line":110,"column":null}}]},"97":{"loc":{"start":{"line":108,"column":54},"end":{"line":110,"column":54}},"type":"binary-expr","locations":[{"start":{"line":108,"column":54},"end":{"line":110,"column":null}},{"start":{"line":110,"column":34},"end":{"line":110,"column":54}}]},"98":{"loc":{"start":{"line":110,"column":54},"end":{"line":112,"column":null}},"type":"cond-expr","locations":[{"start":{"line":112,"column":56},"end":{"line":112,"column":null}},{"start":{"line":112,"column":56},"end":{"line":112,"column":null}}]},"99":{"loc":{"start":{"line":110,"column":54},"end":{"line":112,"column":56}},"type":"binary-expr","locations":[{"start":{"line":110,"column":54},"end":{"line":112,"column":null}},{"start":{"line":112,"column":37},"end":{"line":112,"column":56}}]},"100":{"loc":{"start":{"line":112,"column":56},"end":{"line":114,"column":null}},"type":"cond-expr","locations":[{"start":{"line":114,"column":54},"end":{"line":114,"column":null}},{"start":{"line":114,"column":54},"end":{"line":114,"column":null}}]},"101":{"loc":{"start":{"line":112,"column":56},"end":{"line":114,"column":54}},"type":"binary-expr","locations":[{"start":{"line":112,"column":56},"end":{"line":114,"column":null}},{"start":{"line":114,"column":34},"end":{"line":114,"column":54}}]},"102":{"loc":{"start":{"line":114,"column":54},"end":{"line":116,"column":null}},"type":"cond-expr","locations":[{"start":{"line":116,"column":50},"end":{"line":116,"column":null}},{"start":{"line":116,"column":50},"end":{"line":116,"column":null}}]},"103":{"loc":{"start":{"line":114,"column":54},"end":{"line":116,"column":50}},"type":"binary-expr","locations":[{"start":{"line":114,"column":54},"end":{"line":116,"column":null}},{"start":{"line":116,"column":32},"end":{"line":116,"column":50}}]},"104":{"loc":{"start":{"line":116,"column":50},"end":{"line":118,"column":null}},"type":"cond-expr","locations":[{"start":{"line":118,"column":50},"end":{"line":118,"column":null}},{"start":{"line":118,"column":50},"end":{"line":118,"column":null}}]},"105":{"loc":{"start":{"line":116,"column":50},"end":{"line":118,"column":50}},"type":"binary-expr","locations":[{"start":{"line":116,"column":50},"end":{"line":118,"column":null}},{"start":{"line":118,"column":32},"end":{"line":118,"column":50}}]},"106":{"loc":{"start":{"line":118,"column":50},"end":{"line":120,"column":null}},"type":"cond-expr","locations":[{"start":{"line":120,"column":58},"end":{"line":120,"column":null}},{"start":{"line":120,"column":58},"end":{"line":120,"column":null}}]},"107":{"loc":{"start":{"line":118,"column":50},"end":{"line":120,"column":58}},"type":"binary-expr","locations":[{"start":{"line":118,"column":50},"end":{"line":120,"column":null}},{"start":{"line":120,"column":36},"end":{"line":120,"column":58}}]},"108":{"loc":{"start":{"line":120,"column":58},"end":{"line":122,"column":null}},"type":"cond-expr","locations":[{"start":{"line":122,"column":62},"end":{"line":122,"column":null}},{"start":{"line":122,"column":62},"end":{"line":122,"column":null}}]},"109":{"loc":{"start":{"line":120,"column":58},"end":{"line":122,"column":62}},"type":"binary-expr","locations":[{"start":{"line":120,"column":58},"end":{"line":122,"column":null}},{"start":{"line":122,"column":38},"end":{"line":122,"column":62}}]},"110":{"loc":{"start":{"line":122,"column":62},"end":{"line":124,"column":null}},"type":"cond-expr","locations":[{"start":{"line":124,"column":58},"end":{"line":124,"column":null}},{"start":{"line":124,"column":58},"end":{"line":124,"column":null}}]},"111":{"loc":{"start":{"line":122,"column":62},"end":{"line":124,"column":58}},"type":"binary-expr","locations":[{"start":{"line":122,"column":62},"end":{"line":124,"column":null}},{"start":{"line":124,"column":36},"end":{"line":124,"column":58}}]},"112":{"loc":{"start":{"line":124,"column":58},"end":{"line":126,"column":null}},"type":"cond-expr","locations":[{"start":{"line":126,"column":75},"end":{"line":126,"column":null}},{"start":{"line":126,"column":75},"end":{"line":126,"column":null}}]},"113":{"loc":{"start":{"line":124,"column":58},"end":{"line":126,"column":75}},"type":"binary-expr","locations":[{"start":{"line":124,"column":58},"end":{"line":126,"column":null}},{"start":{"line":126,"column":46},"end":{"line":126,"column":75}}]},"114":{"loc":{"start":{"line":126,"column":75},"end":{"line":128,"column":null}},"type":"cond-expr","locations":[{"start":{"line":128,"column":75},"end":{"line":128,"column":null}},{"start":{"line":128,"column":75},"end":{"line":128,"column":null}}]},"115":{"loc":{"start":{"line":126,"column":75},"end":{"line":128,"column":75}},"type":"binary-expr","locations":[{"start":{"line":126,"column":75},"end":{"line":128,"column":null}},{"start":{"line":128,"column":39},"end":{"line":128,"column":75}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":17,"8":17,"9":17,"10":17,"11":17,"12":17,"13":17,"14":17,"15":17,"16":17,"17":17,"18":17,"19":17,"20":17,"21":17,"22":17,"23":17,"24":17,"25":17,"26":17,"27":17,"28":17,"29":17,"30":17,"31":17,"32":17,"33":17,"34":17,"35":17,"36":17,"37":17,"38":2,"39":1,"40":2,"41":0,"42":2,"43":2,"44":1,"45":12,"46":12,"47":3,"48":9,"49":7,"50":10,"51":0,"52":10,"53":10,"54":8,"55":2,"56":2,"57":0,"58":2,"59":2,"60":2,"61":0,"62":2,"63":2,"64":2,"65":1,"66":1,"67":1,"68":5,"69":5,"70":5,"71":1,"72":4,"73":4,"74":1,"75":1,"76":3,"77":3,"78":1,"79":2,"80":2,"81":2,"82":2,"83":2,"84":2,"85":2,"86":2,"87":2,"88":2,"89":2,"90":2,"91":2,"92":2,"93":2,"94":2,"95":2,"96":2,"97":2,"98":2,"99":2,"100":2,"101":2,"102":2,"103":2,"104":2,"105":2,"106":0,"107":2,"108":0,"109":2,"110":2,"111":0,"112":0,"113":0,"114":2,"115":0,"116":0,"117":0,"118":0,"119":2,"120":0,"121":0,"122":0,"123":0,"124":2,"125":2,"126":2,"127":2,"128":2,"129":2,"130":2,"131":2,"132":2,"133":2,"134":2,"135":2,"136":2,"137":2,"138":2,"139":0,"140":2,"141":2,"142":2,"143":2,"144":2,"145":2,"146":2,"147":2,"148":2,"149":2,"150":2,"151":2,"152":2,"153":2,"154":2,"155":0,"156":2,"157":2,"158":2,"159":2,"160":2,"161":2,"162":2,"163":0,"164":2,"165":0,"166":0,"167":2,"168":2,"169":6,"170":2,"171":0,"172":0,"173":2,"174":0,"175":2,"176":0,"177":2,"178":0,"179":2,"180":0,"181":2,"182":2,"183":2,"184":2,"185":2,"186":2,"187":2,"188":2,"189":2,"190":2,"191":2,"192":2,"193":2,"194":2,"195":2,"196":0,"197":2,"198":2,"199":12,"200":12,"201":12,"202":12,"203":0,"204":12,"205":12,"206":12,"207":52,"208":0,"209":52,"210":52,"211":0,"212":0,"213":0,"214":0,"215":2,"216":2,"217":2,"218":2,"219":2,"220":2,"221":2,"222":2,"223":2,"224":2,"225":2,"226":2,"227":2,"228":2,"229":2,"230":2,"231":2,"232":2,"233":2,"234":2,"235":2,"236":2,"237":2,"238":2,"239":2,"240":2,"241":2,"242":2,"243":2,"244":2,"245":2,"246":2,"247":12,"248":0,"249":12,"250":8,"251":0,"252":8,"253":16,"254":0,"255":16,"256":2,"257":0,"258":0,"259":0,"260":2,"261":2,"262":2,"263":2,"264":2,"265":2,"266":2,"267":2,"268":2,"269":2,"270":2,"271":2,"272":2,"273":2,"274":2,"275":2,"276":2,"277":2,"278":2,"279":2,"280":2,"281":2,"282":17,"283":6,"284":12,"285":6,"286":0,"287":6,"288":6,"289":6,"290":12,"291":6,"292":17,"293":17},"f":{"0":17,"1":2,"2":2,"3":12,"4":10,"5":2,"6":2,"7":5,"8":0,"9":2,"10":2,"11":2,"12":2,"13":0,"14":2,"15":2,"16":0,"17":0,"18":0,"19":6,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":2,"27":2,"28":2,"29":12,"30":12,"31":12,"32":12,"33":52,"34":0,"35":2,"36":2,"37":2,"38":2,"39":2,"40":2,"41":2,"42":2,"43":2,"44":2,"45":12,"46":8,"47":16,"48":2,"49":0,"50":2,"51":2,"52":2,"53":2,"54":2,"55":2,"56":2,"57":2,"58":2,"59":6,"60":12,"61":6},"b":{"0":[0,2],"1":[2,2],"2":[1,1],"3":[3,9],"4":[0,10],"5":[10,10],"6":[8,2],"7":[0,2],"8":[2,2],"9":[0,2],"10":[1,1],"11":[1,4],"12":[1,3],"13":[1,2],"14":[3,3],"15":[2,0],"16":[2,0],"17":[2,0],"18":[2,0],"19":[0,2],"20":[0,2],"21":[0,0],"22":[0,0],"23":[0,0],"24":[0,0],"25":[0,0],"26":[2,0],"27":[2,0],"28":[2,0],"29":[0,2],"30":[0,0],"31":[2,0],"32":[2,0],"33":[2,0],"34":[2,2],"35":[2,0],"36":[2,0],"37":[0,2],"38":[12,12],"39":[12,0],"40":[0,12],"41":[0,52],"42":[52,52],"43":[52,0],"44":[0,0],"45":[0,0],"46":[2,0],"47":[2,0],"48":[2,0],"49":[2,0],"50":[2,0],"51":[2,0],"52":[2,0],"53":[2,0],"54":[2,0],"55":[0,12],"56":[12,12],"57":[0,8],"58":[8,8],"59":[0,16],"60":[0,0],"61":[0,0],"62":[2,2],"63":[2,2],"64":[2,0],"65":[2,0],"66":[2,0],"67":[2,0],"68":[2,0],"69":[2,0],"70":[2,0],"71":[2,0],"72":[2,0],"73":[2,0],"74":[2,0],"75":[2,0],"76":[2,0],"77":[2,0],"78":[2,0],"79":[2,0],"80":[2,0],"81":[2,0],"82":[2,0],"83":[2,0],"84":[12,12],"85":[0,6],"86":[0,17],"87":[17,0],"88":[0,17],"89":[17,0],"90":[0,17],"91":[17,0],"92":[0,17],"93":[17,0],"94":[0,17],"95":[17,0],"96":[0,17],"97":[17,0],"98":[0,17],"99":[17,0],"100":[0,17],"101":[17,0],"102":[0,17],"103":[17,0],"104":[0,17],"105":[17,0],"106":[0,17],"107":[17,0],"108":[0,17],"109":[17,0],"110":[0,17],"111":[17,0],"112":[0,17],"113":[17,0],"114":[0,17],"115":[17,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/text-answer.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/text-answer.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":6,"column":0},"end":{"line":6,"column":72}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":null}},"3":{"start":{"line":12,"column":0},"end":{"line":12,"column":null}},"4":{"start":{"line":19,"column":7},"end":{"line":79,"column":null}},"5":{"start":{"line":22,"column":25},"end":{"line":22,"column":34}},"6":{"start":{"line":24,"column":25},"end":{"line":24,"column":36}},"7":{"start":{"line":31,"column":8},"end":{"line":31,"column":51}},"8":{"start":{"line":33,"column":37},"end":{"line":39,"column":10}},"9":{"start":{"line":41,"column":28},"end":{"line":41,"column":75}},"10":{"start":{"line":42,"column":8},"end":{"line":44,"column":11}},"11":{"start":{"line":43,"column":12},"end":{"line":43,"column":59}},"12":{"start":{"line":46,"column":8},"end":{"line":58,"column":15}},"13":{"start":{"line":48,"column":28},"end":{"line":48,"column":78}},"14":{"start":{"line":51,"column":33},"end":{"line":51,"column":67}},"15":{"start":{"line":52,"column":16},"end":{"line":57,"column":18}},"16":{"start":{"line":67,"column":8},"end":{"line":67,"column":27}},"17":{"start":{"line":67,"column":20},"end":{"line":67,"column":27}},"18":{"start":{"line":70,"column":12},"end":{"line":71,"column":51}},"19":{"start":{"line":73,"column":8},"end":{"line":73,"column":32}},"20":{"start":{"line":73,"column":25},"end":{"line":73,"column":32}},"21":{"start":{"line":75,"column":8},"end":{"line":77,"column":10}},"22":{"start":{"line":19,"column":13},"end":{"line":19,"column":30}},"23":{"start":{"line":19,"column":13},"end":{"line":79,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":20,"column":4},"end":{"line":20,"column":null}},"loc":{"start":{"line":24,"column":72},"end":{"line":25,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":27,"column":4},"end":{"line":27,"column":9}},"loc":{"start":{"line":29,"column":26},"end":{"line":59,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":42,"column":26},"end":{"line":42,"column":27}},"loc":{"start":{"line":42,"column":39},"end":{"line":44,"column":9}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":48,"column":16},"end":{"line":48,"column":17}},"loc":{"start":{"line":48,"column":28},"end":{"line":48,"column":78}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":50,"column":17},"end":{"line":50,"column":18}},"loc":{"start":{"line":50,"column":28},"end":{"line":58,"column":13}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":66,"column":12},"end":{"line":66,"column":17}},"loc":{"start":{"line":66,"column":61},"end":{"line":78,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":48,"column":28},"end":{"line":48,"column":78}},"type":"binary-expr","locations":[{"start":{"line":48,"column":28},"end":{"line":48,"column":44}},{"start":{"line":48,"column":48},"end":{"line":48,"column":78}}]},"1":{"loc":{"start":{"line":54,"column":35},"end":{"line":54,"column":66}},"type":"binary-expr","locations":[{"start":{"line":54,"column":35},"end":{"line":54,"column":58}},{"start":{"line":54,"column":62},"end":{"line":54,"column":66}}]},"2":{"loc":{"start":{"line":67,"column":8},"end":{"line":67,"column":27}},"type":"if","locations":[{"start":{"line":67,"column":8},"end":{"line":67,"column":27}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":70,"column":12},"end":{"line":71,"column":51}},"type":"binary-expr","locations":[{"start":{"line":70,"column":12},"end":{"line":70,"column":54}},{"start":{"line":71,"column":12},"end":{"line":71,"column":51}}]},"4":{"loc":{"start":{"line":73,"column":8},"end":{"line":73,"column":32}},"type":"if","locations":[{"start":{"line":73,"column":8},"end":{"line":73,"column":32}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":22,"column":34},"end":{"line":22,"column":null}},"type":"cond-expr","locations":[{"start":{"line":22,"column":54},"end":{"line":22,"column":null}},{"start":{"line":22,"column":54},"end":{"line":22,"column":null}}]},"6":{"loc":{"start":{"line":22,"column":34},"end":{"line":22,"column":54}},"type":"binary-expr","locations":[{"start":{"line":22,"column":34},"end":{"line":22,"column":null}},{"start":{"line":22,"column":34},"end":{"line":22,"column":54}}]},"7":{"loc":{"start":{"line":22,"column":54},"end":{"line":24,"column":null}},"type":"cond-expr","locations":[{"start":{"line":24,"column":72},"end":{"line":24,"column":null}},{"start":{"line":24,"column":72},"end":{"line":24,"column":null}}]},"8":{"loc":{"start":{"line":22,"column":54},"end":{"line":24,"column":72}},"type":"binary-expr","locations":[{"start":{"line":22,"column":54},"end":{"line":24,"column":null}},{"start":{"line":24,"column":36},"end":{"line":24,"column":72}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":6,"8":5,"9":5,"10":5,"11":5,"12":5,"13":7,"14":5,"15":5,"16":6,"17":1,"18":5,"19":5,"20":4,"21":1,"22":17,"23":17},"f":{"0":17,"1":6,"2":5,"3":7,"4":5,"5":6},"b":{"0":[7,6],"1":[5,1],"2":[1,5],"3":[5,4],"4":[4,1],"5":[0,17],"6":[17,0],"7":[0,17],"8":[17,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/report-analytics.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/report-analytics.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":44}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":75}},"4":{"start":{"line":11,"column":7},"end":{"line":47,"column":null}},"5":{"start":{"line":12,"column":45},"end":{"line":12,"column":72}},"6":{"start":{"line":14,"column":33},"end":{"line":14,"column":41}},"7":{"start":{"line":20,"column":8},"end":{"line":22,"column":9}},"8":{"start":{"line":21,"column":12},"end":{"line":21,"column":19}},"9":{"start":{"line":24,"column":21},"end":{"line":25,"column":null}},"10":{"start":{"line":25,"column":12},"end":{"line":25,"column":58}},"11":{"start":{"line":28,"column":8},"end":{"line":28,"column":63}},"12":{"start":{"line":32,"column":26},"end":{"line":35,"column":10}},"13":{"start":{"line":37,"column":8},"end":{"line":37,"column":61}},"14":{"start":{"line":41,"column":26},"end":{"line":43,"column":10}},"15":{"start":{"line":45,"column":8},"end":{"line":45,"column":76}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":38}},"17":{"start":{"line":11,"column":13},"end":{"line":47,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":14,"column":4},"end":{"line":14,"column":33}},"loc":{"start":{"line":14,"column":54},"end":{"line":14,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":9}},"loc":{"start":{"line":18,"column":42},"end":{"line":29,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":24,"column":35},"end":{"line":24,"column":36}},"loc":{"start":{"line":25,"column":12},"end":{"line":25,"column":58}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":31,"column":4},"end":{"line":31,"column":9}},"loc":{"start":{"line":31,"column":41},"end":{"line":38,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":40,"column":4},"end":{"line":40,"column":9}},"loc":{"start":{"line":40,"column":29},"end":{"line":46,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":20,"column":8},"end":{"line":22,"column":9}},"type":"if","locations":[{"start":{"line":20,"column":8},"end":{"line":22,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":45,"column":15},"end":{"line":45,"column":75}},"type":"cond-expr","locations":[{"start":{"line":45,"column":27},"end":{"line":45,"column":68}},{"start":{"line":45,"column":71},"end":{"line":45,"column":75}}]},"2":{"loc":{"start":{"line":14,"column":41},"end":{"line":14,"column":57}},"type":"cond-expr","locations":[{"start":{"line":14,"column":54},"end":{"line":14,"column":57}},{"start":{"line":14,"column":54},"end":{"line":14,"column":57}}]},"3":{"loc":{"start":{"line":14,"column":41},"end":{"line":14,"column":54}},"type":"binary-expr","locations":[{"start":{"line":14,"column":41},"end":{"line":14,"column":57}},{"start":{"line":14,"column":41},"end":{"line":14,"column":54}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":10,"8":2,"9":8,"10":11,"11":8,"12":6,"13":6,"14":4,"15":4,"16":17,"17":17},"f":{"0":17,"1":10,"2":11,"3":6,"4":4},"b":{"0":[2,8],"1":[2,2],"2":[17,0],"3":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/report-comment.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/report-comment.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":44}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":71}},"4":{"start":{"line":11,"column":7},"end":{"line":39,"column":null}},"5":{"start":{"line":12,"column":43},"end":{"line":12,"column":68}},"6":{"start":{"line":14,"column":33},"end":{"line":14,"column":41}},"7":{"start":{"line":17,"column":24},"end":{"line":19,"column":10}},"8":{"start":{"line":20,"column":8},"end":{"line":20,"column":53}},"9":{"start":{"line":24,"column":25},"end":{"line":27,"column":10}},"10":{"start":{"line":29,"column":8},"end":{"line":29,"column":58}},"11":{"start":{"line":33,"column":24},"end":{"line":35,"column":10}},"12":{"start":{"line":37,"column":8},"end":{"line":37,"column":70}},"13":{"start":{"line":11,"column":13},"end":{"line":11,"column":36}},"14":{"start":{"line":11,"column":13},"end":{"line":39,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":14,"column":4},"end":{"line":14,"column":33}},"loc":{"start":{"line":14,"column":54},"end":{"line":14,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":9}},"loc":{"start":{"line":16,"column":45},"end":{"line":21,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":23,"column":4},"end":{"line":23,"column":9}},"loc":{"start":{"line":23,"column":41},"end":{"line":30,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":32,"column":4},"end":{"line":32,"column":9}},"loc":{"start":{"line":32,"column":29},"end":{"line":38,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":37,"column":15},"end":{"line":37,"column":69}},"type":"cond-expr","locations":[{"start":{"line":37,"column":25},"end":{"line":37,"column":62}},{"start":{"line":37,"column":65},"end":{"line":37,"column":69}}]},"1":{"loc":{"start":{"line":14,"column":41},"end":{"line":14,"column":57}},"type":"cond-expr","locations":[{"start":{"line":14,"column":54},"end":{"line":14,"column":57}},{"start":{"line":14,"column":54},"end":{"line":14,"column":57}}]},"2":{"loc":{"start":{"line":14,"column":41},"end":{"line":14,"column":54}},"type":"binary-expr","locations":[{"start":{"line":14,"column":41},"end":{"line":14,"column":57}},{"start":{"line":14,"column":41},"end":{"line":14,"column":54}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":9,"8":9,"9":4,"10":4,"11":4,"12":4,"13":17,"14":17},"f":{"0":17,"1":9,"2":4,"3":4},"b":{"0":[2,2],"1":[17,0],"2":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/report-insight.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/report-insight.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":44}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":71}},"4":{"start":{"line":11,"column":7},"end":{"line":47,"column":null}},"5":{"start":{"line":12,"column":43},"end":{"line":12,"column":68}},"6":{"start":{"line":14,"column":33},"end":{"line":14,"column":41}},"7":{"start":{"line":20,"column":8},"end":{"line":22,"column":9}},"8":{"start":{"line":21,"column":12},"end":{"line":21,"column":19}},"9":{"start":{"line":24,"column":21},"end":{"line":25,"column":null}},"10":{"start":{"line":25,"column":12},"end":{"line":25,"column":56}},"11":{"start":{"line":28,"column":8},"end":{"line":28,"column":62}},"12":{"start":{"line":32,"column":26},"end":{"line":35,"column":10}},"13":{"start":{"line":37,"column":8},"end":{"line":37,"column":59}},"14":{"start":{"line":41,"column":26},"end":{"line":43,"column":10}},"15":{"start":{"line":45,"column":8},"end":{"line":45,"column":74}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":36}},"17":{"start":{"line":11,"column":13},"end":{"line":47,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":14,"column":4},"end":{"line":14,"column":33}},"loc":{"start":{"line":14,"column":54},"end":{"line":14,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":9}},"loc":{"start":{"line":18,"column":39},"end":{"line":29,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":24,"column":34},"end":{"line":24,"column":35}},"loc":{"start":{"line":25,"column":12},"end":{"line":25,"column":56}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":31,"column":4},"end":{"line":31,"column":9}},"loc":{"start":{"line":31,"column":41},"end":{"line":38,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":40,"column":4},"end":{"line":40,"column":9}},"loc":{"start":{"line":40,"column":29},"end":{"line":46,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":20,"column":8},"end":{"line":22,"column":9}},"type":"if","locations":[{"start":{"line":20,"column":8},"end":{"line":22,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":45,"column":15},"end":{"line":45,"column":73}},"type":"cond-expr","locations":[{"start":{"line":45,"column":27},"end":{"line":45,"column":66}},{"start":{"line":45,"column":69},"end":{"line":45,"column":73}}]},"2":{"loc":{"start":{"line":14,"column":41},"end":{"line":14,"column":57}},"type":"cond-expr","locations":[{"start":{"line":14,"column":54},"end":{"line":14,"column":57}},{"start":{"line":14,"column":54},"end":{"line":14,"column":57}}]},"3":{"loc":{"start":{"line":14,"column":41},"end":{"line":14,"column":54}},"type":"binary-expr","locations":[{"start":{"line":14,"column":41},"end":{"line":14,"column":57}},{"start":{"line":14,"column":41},"end":{"line":14,"column":54}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":8,"8":1,"9":7,"10":23,"11":7,"12":4,"13":4,"14":4,"15":4,"16":17,"17":17},"f":{"0":17,"1":8,"2":23,"3":4,"4":4},"b":{"0":[1,7],"1":[2,2],"2":[17,0],"3":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/report.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/report.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":44}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":60}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":null}},"4":{"start":{"line":14,"column":0},"end":{"line":14,"column":56}},"5":{"start":{"line":17,"column":7},"end":{"line":144,"column":null}},"6":{"start":{"line":18,"column":35},"end":{"line":18,"column":52}},"7":{"start":{"line":20,"column":33},"end":{"line":20,"column":41}},"8":{"start":{"line":23,"column":22},"end":{"line":23,"column":44}},"9":{"start":{"line":24,"column":24},"end":{"line":24,"column":46}},"10":{"start":{"line":25,"column":22},"end":{"line":25,"column":75}},"11":{"start":{"line":26,"column":8},"end":{"line":26,"column":48}},"12":{"start":{"line":30,"column":24},"end":{"line":37,"column":10}},"13":{"start":{"line":39,"column":8},"end":{"line":39,"column":59}},"14":{"start":{"line":43,"column":23},"end":{"line":50,"column":10}},"15":{"start":{"line":52,"column":8},"end":{"line":52,"column":74}},"16":{"start":{"line":56,"column":23},"end":{"line":63,"column":10}},"17":{"start":{"line":65,"column":8},"end":{"line":65,"column":74}},"18":{"start":{"line":94,"column":12},"end":{"line":94,"column":17}},"19":{"start":{"line":95,"column":8},"end":{"line":134,"column":10}},"20":{"start":{"line":103,"column":30},"end":{"line":103,"column":72}},"21":{"start":{"line":140,"column":22},"end":{"line":140,"column":56}},"22":{"start":{"line":141,"column":26},"end":{"line":141,"column":66}},"23":{"start":{"line":142,"column":8},"end":{"line":142,"column":74}},"24":{"start":{"line":17,"column":13},"end":{"line":17,"column":29}},"25":{"start":{"line":17,"column":13},"end":{"line":144,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":20,"column":4},"end":{"line":20,"column":33}},"loc":{"start":{"line":20,"column":54},"end":{"line":20,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":22,"column":4},"end":{"line":22,"column":9}},"loc":{"start":{"line":22,"column":41},"end":{"line":27,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":29,"column":4},"end":{"line":29,"column":9}},"loc":{"start":{"line":29,"column":37},"end":{"line":40,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":42,"column":4},"end":{"line":42,"column":9}},"loc":{"start":{"line":42,"column":29},"end":{"line":53,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":55,"column":4},"end":{"line":55,"column":9}},"loc":{"start":{"line":55,"column":41},"end":{"line":66,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":68,"column":12},"end":{"line":68,"column":22}},"loc":{"start":{"line":68,"column":47},"end":{"line":135,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":102,"column":60},"end":{"line":102,"column":61}},"loc":{"start":{"line":103,"column":30},"end":{"line":103,"column":72}}},"7":{"name":"(anonymous_9)","decl":{"start":{"line":137,"column":12},"end":{"line":137,"column":22}},"loc":{"start":{"line":138,"column":32},"end":{"line":143,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":52,"column":15},"end":{"line":52,"column":73}},"type":"cond-expr","locations":[{"start":{"line":52,"column":24},"end":{"line":52,"column":66}},{"start":{"line":52,"column":69},"end":{"line":52,"column":73}}]},"1":{"loc":{"start":{"line":65,"column":15},"end":{"line":65,"column":73}},"type":"cond-expr","locations":[{"start":{"line":65,"column":24},"end":{"line":65,"column":66}},{"start":{"line":65,"column":69},"end":{"line":65,"column":73}}]},"2":{"loc":{"start":{"line":96,"column":16},"end":{"line":96,"column":44}},"type":"cond-expr","locations":[{"start":{"line":96,"column":27},"end":{"line":96,"column":39}},{"start":{"line":96,"column":42},"end":{"line":96,"column":44}}]},"3":{"loc":{"start":{"line":97,"column":16},"end":{"line":97,"column":42}},"type":"cond-expr","locations":[{"start":{"line":97,"column":26},"end":{"line":97,"column":37}},{"start":{"line":97,"column":40},"end":{"line":97,"column":42}}]},"4":{"loc":{"start":{"line":98,"column":16},"end":{"line":98,"column":58}},"type":"cond-expr","locations":[{"start":{"line":98,"column":34},"end":{"line":98,"column":53}},{"start":{"line":98,"column":56},"end":{"line":98,"column":58}}]},"5":{"loc":{"start":{"line":99,"column":16},"end":{"line":107,"column":20}},"type":"cond-expr","locations":[{"start":{"line":100,"column":18},"end":{"line":106,"column":null}},{"start":{"line":107,"column":18},"end":{"line":107,"column":20}}]},"6":{"loc":{"start":{"line":108,"column":16},"end":{"line":108,"column":50}},"type":"cond-expr","locations":[{"start":{"line":108,"column":30},"end":{"line":108,"column":45}},{"start":{"line":108,"column":48},"end":{"line":108,"column":50}}]},"7":{"loc":{"start":{"line":109,"column":16},"end":{"line":109,"column":60}},"type":"cond-expr","locations":[{"start":{"line":109,"column":35},"end":{"line":109,"column":55}},{"start":{"line":109,"column":58},"end":{"line":109,"column":60}}]},"8":{"loc":{"start":{"line":110,"column":16},"end":{"line":110,"column":62}},"type":"cond-expr","locations":[{"start":{"line":110,"column":36},"end":{"line":110,"column":57}},{"start":{"line":110,"column":60},"end":{"line":110,"column":62}}]},"9":{"loc":{"start":{"line":111,"column":16},"end":{"line":111,"column":68}},"type":"cond-expr","locations":[{"start":{"line":111,"column":39},"end":{"line":111,"column":63}},{"start":{"line":111,"column":66},"end":{"line":111,"column":68}}]},"10":{"loc":{"start":{"line":112,"column":16},"end":{"line":112,"column":68}},"type":"cond-expr","locations":[{"start":{"line":112,"column":39},"end":{"line":112,"column":63}},{"start":{"line":112,"column":66},"end":{"line":112,"column":68}}]},"11":{"loc":{"start":{"line":113,"column":16},"end":{"line":113,"column":72}},"type":"cond-expr","locations":[{"start":{"line":113,"column":41},"end":{"line":113,"column":67}},{"start":{"line":113,"column":70},"end":{"line":113,"column":72}}]},"12":{"loc":{"start":{"line":114,"column":16},"end":{"line":114,"column":68}},"type":"cond-expr","locations":[{"start":{"line":114,"column":39},"end":{"line":114,"column":63}},{"start":{"line":114,"column":66},"end":{"line":114,"column":68}}]},"13":{"loc":{"start":{"line":115,"column":16},"end":{"line":115,"column":68}},"type":"cond-expr","locations":[{"start":{"line":115,"column":39},"end":{"line":115,"column":63}},{"start":{"line":115,"column":66},"end":{"line":115,"column":68}}]},"14":{"loc":{"start":{"line":116,"column":16},"end":{"line":116,"column":72}},"type":"cond-expr","locations":[{"start":{"line":116,"column":41},"end":{"line":116,"column":67}},{"start":{"line":116,"column":70},"end":{"line":116,"column":72}}]},"15":{"loc":{"start":{"line":117,"column":16},"end":{"line":117,"column":78}},"type":"cond-expr","locations":[{"start":{"line":117,"column":44},"end":{"line":117,"column":73}},{"start":{"line":117,"column":76},"end":{"line":117,"column":78}}]},"16":{"loc":{"start":{"line":118,"column":16},"end":{"line":120,"column":20}},"type":"cond-expr","locations":[{"start":{"line":119,"column":18},"end":{"line":119,"column":49}},{"start":{"line":120,"column":18},"end":{"line":120,"column":20}}]},"17":{"loc":{"start":{"line":121,"column":16},"end":{"line":121,"column":72}},"type":"cond-expr","locations":[{"start":{"line":121,"column":41},"end":{"line":121,"column":67}},{"start":{"line":121,"column":70},"end":{"line":121,"column":72}}]},"18":{"loc":{"start":{"line":122,"column":16},"end":{"line":122,"column":72}},"type":"cond-expr","locations":[{"start":{"line":122,"column":41},"end":{"line":122,"column":67}},{"start":{"line":122,"column":70},"end":{"line":122,"column":72}}]},"19":{"loc":{"start":{"line":123,"column":16},"end":{"line":123,"column":76}},"type":"cond-expr","locations":[{"start":{"line":123,"column":43},"end":{"line":123,"column":71}},{"start":{"line":123,"column":74},"end":{"line":123,"column":76}}]},"20":{"loc":{"start":{"line":124,"column":16},"end":{"line":124,"column":72}},"type":"cond-expr","locations":[{"start":{"line":124,"column":41},"end":{"line":124,"column":67}},{"start":{"line":124,"column":70},"end":{"line":124,"column":72}}]},"21":{"loc":{"start":{"line":125,"column":16},"end":{"line":125,"column":72}},"type":"cond-expr","locations":[{"start":{"line":125,"column":41},"end":{"line":125,"column":67}},{"start":{"line":125,"column":70},"end":{"line":125,"column":72}}]},"22":{"loc":{"start":{"line":126,"column":16},"end":{"line":126,"column":76}},"type":"cond-expr","locations":[{"start":{"line":126,"column":43},"end":{"line":126,"column":71}},{"start":{"line":126,"column":74},"end":{"line":126,"column":76}}]},"23":{"loc":{"start":{"line":127,"column":16},"end":{"line":129,"column":20}},"type":"cond-expr","locations":[{"start":{"line":128,"column":18},"end":{"line":128,"column":49}},{"start":{"line":129,"column":18},"end":{"line":129,"column":20}}]},"24":{"loc":{"start":{"line":130,"column":16},"end":{"line":132,"column":20}},"type":"cond-expr","locations":[{"start":{"line":131,"column":18},"end":{"line":131,"column":51}},{"start":{"line":132,"column":18},"end":{"line":132,"column":20}}]},"25":{"loc":{"start":{"line":133,"column":16},"end":{"line":133,"column":46}},"type":"cond-expr","locations":[{"start":{"line":133,"column":28},"end":{"line":133,"column":41}},{"start":{"line":133,"column":44},"end":{"line":133,"column":46}}]},"26":{"loc":{"start":{"line":140,"column":22},"end":{"line":140,"column":56}},"type":"binary-expr","locations":[{"start":{"line":140,"column":22},"end":{"line":140,"column":34}},{"start":{"line":140,"column":38},"end":{"line":140,"column":56}}]},"27":{"loc":{"start":{"line":141,"column":26},"end":{"line":141,"column":66}},"type":"binary-expr","locations":[{"start":{"line":141,"column":26},"end":{"line":141,"column":45}},{"start":{"line":141,"column":49},"end":{"line":141,"column":66}}]},"28":{"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":57}},"type":"cond-expr","locations":[{"start":{"line":20,"column":54},"end":{"line":20,"column":57}},{"start":{"line":20,"column":54},"end":{"line":20,"column":57}}]},"29":{"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":54}},"type":"binary-expr","locations":[{"start":{"line":20,"column":41},"end":{"line":20,"column":57}},{"start":{"line":20,"column":41},"end":{"line":20,"column":54}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":17,"8":9,"9":9,"10":9,"11":9,"12":59,"13":59,"14":20,"15":20,"16":10,"17":10,"18":9,"19":9,"20":0,"21":9,"22":9,"23":9,"24":17,"25":17},"f":{"0":17,"1":9,"2":59,"3":20,"4":10,"5":9,"6":0,"7":9},"b":{"0":[15,5],"1":[4,6],"2":[0,9],"3":[4,5],"4":[1,8],"5":[0,9],"6":[0,9],"7":[0,9],"8":[0,9],"9":[0,9],"10":[0,9],"11":[0,9],"12":[0,9],"13":[0,9],"14":[0,9],"15":[0,9],"16":[0,9],"17":[0,9],"18":[0,9],"19":[0,9],"20":[0,9],"21":[0,9],"22":[0,9],"23":[0,9],"24":[0,9],"25":[0,9],"26":[9,8],"27":[9,8],"28":[17,0],"29":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":44}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":90}},"4":{"start":{"line":11,"column":7},"end":{"line":52,"column":null}},"5":{"start":{"line":13,"column":8},"end":{"line":13,"column":43}},"6":{"start":{"line":15,"column":33},"end":{"line":15,"column":41}},"7":{"start":{"line":21,"column":8},"end":{"line":23,"column":9}},"8":{"start":{"line":22,"column":12},"end":{"line":22,"column":19}},"9":{"start":{"line":25,"column":21},"end":{"line":26,"column":null}},"10":{"start":{"line":26,"column":12},"end":{"line":26,"column":74}},"11":{"start":{"line":29,"column":8},"end":{"line":29,"column":71}},"12":{"start":{"line":35,"column":26},"end":{"line":38,"column":10}},"13":{"start":{"line":40,"column":8},"end":{"line":40,"column":68}},"14":{"start":{"line":44,"column":26},"end":{"line":46,"column":10}},"15":{"start":{"line":48,"column":8},"end":{"line":50,"column":19}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":45}},"17":{"start":{"line":11,"column":13},"end":{"line":52,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":15,"column":4},"end":{"line":15,"column":33}},"loc":{"start":{"line":15,"column":54},"end":{"line":15,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":17,"column":4},"end":{"line":17,"column":9}},"loc":{"start":{"line":19,"column":48},"end":{"line":30,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":25,"column":34},"end":{"line":25,"column":35}},"loc":{"start":{"line":26,"column":12},"end":{"line":26,"column":74}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":32,"column":4},"end":{"line":32,"column":9}},"loc":{"start":{"line":33,"column":24},"end":{"line":41,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":43,"column":4},"end":{"line":43,"column":9}},"loc":{"start":{"line":43,"column":29},"end":{"line":51,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":21,"column":8},"end":{"line":23,"column":9}},"type":"if","locations":[{"start":{"line":21,"column":8},"end":{"line":23,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":48,"column":15},"end":{"line":50,"column":18}},"type":"cond-expr","locations":[{"start":{"line":49,"column":14},"end":{"line":49,"column":62}},{"start":{"line":50,"column":14},"end":{"line":50,"column":18}}]},"2":{"loc":{"start":{"line":15,"column":41},"end":{"line":15,"column":57}},"type":"cond-expr","locations":[{"start":{"line":15,"column":54},"end":{"line":15,"column":57}},{"start":{"line":15,"column":54},"end":{"line":15,"column":57}}]},"3":{"loc":{"start":{"line":15,"column":41},"end":{"line":15,"column":54}},"type":"binary-expr","locations":[{"start":{"line":15,"column":41},"end":{"line":15,"column":57}},{"start":{"line":15,"column":41},"end":{"line":15,"column":54}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":8,"8":1,"9":7,"10":15,"11":7,"12":4,"13":4,"14":4,"15":4,"16":17,"17":17},"f":{"0":17,"1":8,"2":15,"3":4,"4":4},"b":{"0":[1,7],"1":[2,2],"2":[17,0],"3":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":44}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":94}},"4":{"start":{"line":11,"column":7},"end":{"line":54,"column":null}},"5":{"start":{"line":13,"column":8},"end":{"line":13,"column":45}},"6":{"start":{"line":15,"column":33},"end":{"line":15,"column":41}},"7":{"start":{"line":21,"column":8},"end":{"line":23,"column":9}},"8":{"start":{"line":22,"column":12},"end":{"line":22,"column":19}},"9":{"start":{"line":25,"column":21},"end":{"line":26,"column":null}},"10":{"start":{"line":26,"column":12},"end":{"line":26,"column":67}},"11":{"start":{"line":29,"column":8},"end":{"line":29,"column":72}},"12":{"start":{"line":35,"column":26},"end":{"line":38,"column":10}},"13":{"start":{"line":40,"column":8},"end":{"line":40,"column":70}},"14":{"start":{"line":44,"column":26},"end":{"line":47,"column":null}},"15":{"start":{"line":50,"column":8},"end":{"line":52,"column":19}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":47}},"17":{"start":{"line":11,"column":13},"end":{"line":54,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":15,"column":4},"end":{"line":15,"column":33}},"loc":{"start":{"line":15,"column":54},"end":{"line":15,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":17,"column":4},"end":{"line":17,"column":9}},"loc":{"start":{"line":19,"column":51},"end":{"line":30,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":25,"column":35},"end":{"line":25,"column":36}},"loc":{"start":{"line":26,"column":12},"end":{"line":26,"column":67}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":32,"column":4},"end":{"line":32,"column":9}},"loc":{"start":{"line":33,"column":33},"end":{"line":41,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":43,"column":4},"end":{"line":43,"column":9}},"loc":{"start":{"line":43,"column":29},"end":{"line":53,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":21,"column":8},"end":{"line":23,"column":9}},"type":"if","locations":[{"start":{"line":21,"column":8},"end":{"line":23,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":50,"column":15},"end":{"line":52,"column":18}},"type":"cond-expr","locations":[{"start":{"line":51,"column":14},"end":{"line":51,"column":64}},{"start":{"line":52,"column":14},"end":{"line":52,"column":18}}]},"2":{"loc":{"start":{"line":15,"column":41},"end":{"line":15,"column":57}},"type":"cond-expr","locations":[{"start":{"line":15,"column":54},"end":{"line":15,"column":57}},{"start":{"line":15,"column":54},"end":{"line":15,"column":57}}]},"3":{"loc":{"start":{"line":15,"column":41},"end":{"line":15,"column":54}},"type":"binary-expr","locations":[{"start":{"line":15,"column":41},"end":{"line":15,"column":57}},{"start":{"line":15,"column":41},"end":{"line":15,"column":54}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":8,"8":1,"9":7,"10":9,"11":7,"12":4,"13":4,"14":4,"15":4,"16":17,"17":17},"f":{"0":17,"1":8,"2":9,"3":4,"4":4},"b":{"0":[1,7],"1":[2,2],"2":[17,0],"3":[17,17]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":44}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":60}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":null}},"4":{"start":{"line":14,"column":0},"end":{"line":14,"column":75}},"5":{"start":{"line":17,"column":7},"end":{"line":161,"column":null}},"6":{"start":{"line":18,"column":45},"end":{"line":18,"column":72}},"7":{"start":{"line":20,"column":33},"end":{"line":20,"column":41}},"8":{"start":{"line":25,"column":22},"end":{"line":25,"column":44}},"9":{"start":{"line":26,"column":24},"end":{"line":26,"column":46}},"10":{"start":{"line":27,"column":22},"end":{"line":30,"column":10}},"11":{"start":{"line":31,"column":8},"end":{"line":31,"column":57}},"12":{"start":{"line":37,"column":24},"end":{"line":43,"column":10}},"13":{"start":{"line":45,"column":8},"end":{"line":45,"column":68}},"14":{"start":{"line":49,"column":23},"end":{"line":55,"column":10}},"15":{"start":{"line":57,"column":8},"end":{"line":59,"column":19}},"16":{"start":{"line":65,"column":23},"end":{"line":72,"column":10}},"17":{"start":{"line":74,"column":8},"end":{"line":76,"column":19}},"18":{"start":{"line":112,"column":12},"end":{"line":112,"column":17}},"19":{"start":{"line":113,"column":8},"end":{"line":151,"column":10}},"20":{"start":{"line":157,"column":22},"end":{"line":157,"column":65}},"21":{"start":{"line":158,"column":26},"end":{"line":158,"column":66}},"22":{"start":{"line":159,"column":8},"end":{"line":159,"column":74}},"23":{"start":{"line":17,"column":13},"end":{"line":17,"column":38}},"24":{"start":{"line":17,"column":13},"end":{"line":161,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":20,"column":4},"end":{"line":20,"column":33}},"loc":{"start":{"line":20,"column":54},"end":{"line":20,"column":58}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":22,"column":4},"end":{"line":22,"column":9}},"loc":{"start":{"line":23,"column":41},"end":{"line":32,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":34,"column":4},"end":{"line":34,"column":9}},"loc":{"start":{"line":35,"column":37},"end":{"line":46,"column":5}}},"3":{"name":"(anonymous_5)","decl":{"start":{"line":48,"column":4},"end":{"line":48,"column":9}},"loc":{"start":{"line":48,"column":29},"end":{"line":60,"column":5}}},"4":{"name":"(anonymous_6)","decl":{"start":{"line":62,"column":4},"end":{"line":62,"column":9}},"loc":{"start":{"line":63,"column":23},"end":{"line":77,"column":5}}},"5":{"name":"(anonymous_7)","decl":{"start":{"line":79,"column":12},"end":{"line":79,"column":22}},"loc":{"start":{"line":80,"column":41},"end":{"line":152,"column":5}}},"6":{"name":"(anonymous_8)","decl":{"start":{"line":154,"column":12},"end":{"line":154,"column":22}},"loc":{"start":{"line":155,"column":41},"end":{"line":160,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":57,"column":15},"end":{"line":59,"column":18}},"type":"cond-expr","locations":[{"start":{"line":58,"column":14},"end":{"line":58,"column":65}},{"start":{"line":59,"column":14},"end":{"line":59,"column":18}}]},"1":{"loc":{"start":{"line":74,"column":15},"end":{"line":76,"column":18}},"type":"cond-expr","locations":[{"start":{"line":75,"column":14},"end":{"line":75,"column":65}},{"start":{"line":76,"column":14},"end":{"line":76,"column":18}}]},"2":{"loc":{"start":{"line":114,"column":16},"end":{"line":114,"column":42}},"type":"cond-expr","locations":[{"start":{"line":114,"column":26},"end":{"line":114,"column":37}},{"start":{"line":114,"column":40},"end":{"line":114,"column":42}}]},"3":{"loc":{"start":{"line":115,"column":16},"end":{"line":115,"column":48}},"type":"cond-expr","locations":[{"start":{"line":115,"column":29},"end":{"line":115,"column":43}},{"start":{"line":115,"column":46},"end":{"line":115,"column":48}}]},"4":{"loc":{"start":{"line":116,"column":16},"end":{"line":116,"column":67}},"type":"cond-expr","locations":[{"start":{"line":116,"column":27},"end":{"line":116,"column":62}},{"start":{"line":116,"column":65},"end":{"line":116,"column":67}}]},"5":{"loc":{"start":{"line":117,"column":16},"end":{"line":117,"column":48}},"type":"cond-expr","locations":[{"start":{"line":117,"column":29},"end":{"line":117,"column":43}},{"start":{"line":117,"column":46},"end":{"line":117,"column":48}}]},"6":{"loc":{"start":{"line":118,"column":16},"end":{"line":120,"column":20}},"type":"cond-expr","locations":[{"start":{"line":119,"column":18},"end":{"line":119,"column":63}},{"start":{"line":120,"column":18},"end":{"line":120,"column":20}}]},"7":{"loc":{"start":{"line":121,"column":16},"end":{"line":121,"column":58}},"type":"cond-expr","locations":[{"start":{"line":121,"column":34},"end":{"line":121,"column":53}},{"start":{"line":121,"column":56},"end":{"line":121,"column":58}}]},"8":{"loc":{"start":{"line":122,"column":16},"end":{"line":122,"column":50}},"type":"cond-expr","locations":[{"start":{"line":122,"column":30},"end":{"line":122,"column":45}},{"start":{"line":122,"column":48},"end":{"line":122,"column":50}}]},"9":{"loc":{"start":{"line":123,"column":16},"end":{"line":123,"column":76}},"type":"cond-expr","locations":[{"start":{"line":123,"column":30},"end":{"line":123,"column":71}},{"start":{"line":123,"column":74},"end":{"line":123,"column":76}}]},"10":{"loc":{"start":{"line":124,"column":16},"end":{"line":124,"column":54}},"type":"cond-expr","locations":[{"start":{"line":124,"column":32},"end":{"line":124,"column":49}},{"start":{"line":124,"column":52},"end":{"line":124,"column":54}}]},"11":{"loc":{"start":{"line":125,"column":16},"end":{"line":125,"column":64}},"type":"cond-expr","locations":[{"start":{"line":125,"column":26},"end":{"line":125,"column":59}},{"start":{"line":125,"column":62},"end":{"line":125,"column":64}}]},"12":{"loc":{"start":{"line":126,"column":16},"end":{"line":126,"column":46}},"type":"cond-expr","locations":[{"start":{"line":126,"column":28},"end":{"line":126,"column":41}},{"start":{"line":126,"column":44},"end":{"line":126,"column":46}}]},"13":{"loc":{"start":{"line":127,"column":16},"end":{"line":127,"column":76}},"type":"cond-expr","locations":[{"start":{"line":127,"column":30},"end":{"line":127,"column":71}},{"start":{"line":127,"column":74},"end":{"line":127,"column":76}}]},"14":{"loc":{"start":{"line":128,"column":16},"end":{"line":128,"column":54}},"type":"cond-expr","locations":[{"start":{"line":128,"column":32},"end":{"line":128,"column":49}},{"start":{"line":128,"column":52},"end":{"line":128,"column":54}}]},"15":{"loc":{"start":{"line":129,"column":16},"end":{"line":131,"column":20}},"type":"cond-expr","locations":[{"start":{"line":130,"column":18},"end":{"line":130,"column":63}},{"start":{"line":131,"column":18},"end":{"line":131,"column":20}}]},"16":{"loc":{"start":{"line":132,"column":16},"end":{"line":132,"column":58}},"type":"cond-expr","locations":[{"start":{"line":132,"column":34},"end":{"line":132,"column":53}},{"start":{"line":132,"column":56},"end":{"line":132,"column":58}}]},"17":{"loc":{"start":{"line":133,"column":16},"end":{"line":133,"column":76}},"type":"cond-expr","locations":[{"start":{"line":133,"column":30},"end":{"line":133,"column":71}},{"start":{"line":133,"column":74},"end":{"line":133,"column":76}}]},"18":{"loc":{"start":{"line":134,"column":16},"end":{"line":134,"column":54}},"type":"cond-expr","locations":[{"start":{"line":134,"column":32},"end":{"line":134,"column":49}},{"start":{"line":134,"column":52},"end":{"line":134,"column":54}}]},"19":{"loc":{"start":{"line":135,"column":16},"end":{"line":135,"column":70}},"type":"cond-expr","locations":[{"start":{"line":135,"column":40},"end":{"line":135,"column":65}},{"start":{"line":135,"column":68},"end":{"line":135,"column":70}}]},"20":{"loc":{"start":{"line":136,"column":16},"end":{"line":136,"column":68}},"type":"cond-expr","locations":[{"start":{"line":136,"column":39},"end":{"line":136,"column":63}},{"start":{"line":136,"column":66},"end":{"line":136,"column":68}}]},"21":{"loc":{"start":{"line":137,"column":16},"end":{"line":137,"column":70}},"type":"cond-expr","locations":[{"start":{"line":137,"column":40},"end":{"line":137,"column":65}},{"start":{"line":137,"column":68},"end":{"line":137,"column":70}}]},"22":{"loc":{"start":{"line":138,"column":16},"end":{"line":138,"column":46}},"type":"cond-expr","locations":[{"start":{"line":138,"column":28},"end":{"line":138,"column":41}},{"start":{"line":138,"column":44},"end":{"line":138,"column":46}}]},"23":{"loc":{"start":{"line":139,"column":16},"end":{"line":139,"column":76}},"type":"cond-expr","locations":[{"start":{"line":139,"column":43},"end":{"line":139,"column":71}},{"start":{"line":139,"column":74},"end":{"line":139,"column":76}}]},"24":{"loc":{"start":{"line":140,"column":16},"end":{"line":140,"column":76}},"type":"cond-expr","locations":[{"start":{"line":140,"column":43},"end":{"line":140,"column":71}},{"start":{"line":140,"column":74},"end":{"line":140,"column":76}}]},"25":{"loc":{"start":{"line":141,"column":16},"end":{"line":141,"column":78}},"type":"cond-expr","locations":[{"start":{"line":141,"column":44},"end":{"line":141,"column":73}},{"start":{"line":141,"column":76},"end":{"line":141,"column":78}}]},"26":{"loc":{"start":{"line":142,"column":16},"end":{"line":142,"column":76}},"type":"cond-expr","locations":[{"start":{"line":142,"column":43},"end":{"line":142,"column":71}},{"start":{"line":142,"column":74},"end":{"line":142,"column":76}}]},"27":{"loc":{"start":{"line":143,"column":16},"end":{"line":143,"column":76}},"type":"cond-expr","locations":[{"start":{"line":143,"column":43},"end":{"line":143,"column":71}},{"start":{"line":143,"column":74},"end":{"line":143,"column":76}}]},"28":{"loc":{"start":{"line":144,"column":16},"end":{"line":144,"column":78}},"type":"cond-expr","locations":[{"start":{"line":144,"column":44},"end":{"line":144,"column":73}},{"start":{"line":144,"column":76},"end":{"line":144,"column":78}}]},"29":{"loc":{"start":{"line":145,"column":16},"end":{"line":147,"column":20}},"type":"cond-expr","locations":[{"start":{"line":146,"column":18},"end":{"line":146,"column":48}},{"start":{"line":147,"column":18},"end":{"line":147,"column":20}}]},"30":{"loc":{"start":{"line":148,"column":16},"end":{"line":150,"column":20}},"type":"cond-expr","locations":[{"start":{"line":149,"column":18},"end":{"line":149,"column":49}},{"start":{"line":150,"column":18},"end":{"line":150,"column":20}}]},"31":{"loc":{"start":{"line":157,"column":22},"end":{"line":157,"column":65}},"type":"binary-expr","locations":[{"start":{"line":157,"column":22},"end":{"line":157,"column":34}},{"start":{"line":157,"column":38},"end":{"line":157,"column":65}}]},"32":{"loc":{"start":{"line":158,"column":26},"end":{"line":158,"column":66}},"type":"binary-expr","locations":[{"start":{"line":158,"column":26},"end":{"line":158,"column":45}},{"start":{"line":158,"column":49},"end":{"line":158,"column":66}}]},"33":{"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":57}},"type":"cond-expr","locations":[{"start":{"line":20,"column":54},"end":{"line":20,"column":57}},{"start":{"line":20,"column":54},"end":{"line":20,"column":57}}]},"34":{"loc":{"start":{"line":20,"column":41},"end":{"line":20,"column":54}},"type":"binary-expr","locations":[{"start":{"line":20,"column":41},"end":{"line":20,"column":57}},{"start":{"line":20,"column":41},"end":{"line":20,"column":54}}]}},"s":{"0":17,"1":17,"2":17,"3":17,"4":17,"5":17,"6":17,"7":17,"8":5,"9":5,"10":5,"11":5,"12":42,"13":42,"14":16,"15":16,"16":11,"17":11,"18":5,"19":5,"20":5,"21":5,"22":5,"23":17,"24":17},"f":{"0":17,"1":5,"2":42,"3":16,"4":11,"5":5,"6":5},"b":{"0":[12,4],"1":[5,6],"2":[1,4],"3":[0,5],"4":[1,4],"5":[0,5],"6":[0,5],"7":[0,5],"8":[0,5],"9":[0,5],"10":[0,5],"11":[0,5],"12":[0,5],"13":[0,5],"14":[0,5],"15":[0,5],"16":[0,5],"17":[0,5],"18":[0,5],"19":[0,5],"20":[0,5],"21":[0,5],"22":[0,5],"23":[0,5],"24":[0,5],"25":[0,5],"26":[0,5],"27":[0,5],"28":[0,5],"29":[0,5],"30":[0,5],"31":[5,4],"32":[5,4],"33":[17,0],"34":[17,17]}} +} diff --git a/apps/api/test/integration/coverage/lcov-report/base.css b/apps/api/test/integration/coverage/lcov-report/base.css new file mode 100644 index 00000000..f418035b --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/apps/api/test/integration/coverage/lcov-report/block-navigation.js b/apps/api/test/integration/coverage/lcov-report/block-navigation.js new file mode 100644 index 00000000..530d1ed2 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/apps/api/test/integration/coverage/lcov-report/favicon.png b/apps/api/test/integration/coverage/lcov-report/favicon.png new file mode 100644 index 00000000..c1525b81 Binary files /dev/null and b/apps/api/test/integration/coverage/lcov-report/favicon.png differ diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/cycle-stage-transitions.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/cycle-stage-transitions.ts.html new file mode 100644 index 00000000..7a47c053 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/cycle-stage-transitions.ts.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for feedback360/application/constants/cycle-stage-transitions.ts + + + + + + + + + +
+
+

All files / feedback360/application/constants cycle-stage-transitions.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +3312x +  +  +  +  +  +12x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+ 
+/**
+ * State machine transition map for Cycle
+ * Defines which stage transitions are allowed
+ */
+export const CYCLE_STAGE_TRANSITIONS: Record<CycleStage, CycleStage[]> = {
+    [CycleStage.NEW]: [CycleStage.ACTIVE, CycleStage.CANCELED],
+ 
+    [CycleStage.ACTIVE]: [CycleStage.FINISHED, CycleStage.CANCELED],
+ 
+    [CycleStage.FINISHED]: [CycleStage.PREPARING_REPORT, CycleStage.ARCHIVED],
+ 
+    [CycleStage.PREPARING_REPORT]: [CycleStage.PUBLISHED, CycleStage.ARCHIVED],
+ 
+    [CycleStage.PUBLISHED]: [CycleStage.ARCHIVED],
+ 
+    [CycleStage.CANCELED]: [CycleStage.ARCHIVED],
+ 
+    [CycleStage.ARCHIVED]: [],
+};
+ 
+/**
+ * Transition reasons for automated stage changes
+ */
+export const TRANSITION_REASONS = {
+    CYCLE_UPDATED: 'Cycle updated',
+    ALL_REVIEWS_COLLECTED: 'All reviews collected',
+    DEADLINE_REACHED: 'Cycle deadline has been reached',
+    FORCE_FINISH: 'Manually finished by HR or Admin',
+    SYSTEM_AUTOMATED: 'Automated system transition',
+} as const;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/index.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/index.html new file mode 100644 index 00000000..51f15761 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for feedback360/application/constants + + + + + + + + + +
+
+

All files feedback360/application/constants

+
+ +
+ 33.33% + Statements + 3/9 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 33.33% + Lines + 3/9 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage-transitions.ts +
+
100%3/3100%0/0100%0/0100%3/3
response-status-transitions.ts +
+
0%0/3100%0/0100%0/00%0/3
review-stage-transitions.ts +
+
0%0/3100%0/0100%0/00%0/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/response-status-transitions.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/response-status-transitions.ts.html new file mode 100644 index 00000000..e2c53edf --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/response-status-transitions.ts.html @@ -0,0 +1,187 @@ + + + + + + Code coverage report for feedback360/application/constants/response-status-transitions.ts + + + + + + + + + +
+
+

All files / feedback360/application/constants response-status-transitions.ts

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { ResponseStatus } from '@intra/shared-kernel';
+ 
+/**
+ * State machine transition map for respondent response status
+ * Defines which stage transitions are allowed
+ */
+export const RESPONSE_STATUS_TRANSITIONS: Record<
+    ResponseStatus,
+    ResponseStatus[]
+> = {
+    [ResponseStatus.PENDING]: [
+        ResponseStatus.IN_PROGRESS,
+        ResponseStatus.CANCELED,
+    ],
+ 
+    [ResponseStatus.IN_PROGRESS]: [
+        ResponseStatus.COMPLETED,
+        ResponseStatus.CANCELED,
+    ],
+ 
+    [ResponseStatus.COMPLETED]: [],
+ 
+    [ResponseStatus.CANCELED]: [],
+};
+ 
+/**
+ * Transition reasons for automated stage changes
+ */
+export const TRANSITION_REASONS = {
+    RESPONSE_ASSIGNED: 'HR or Admin assigned review to respondent',
+    RESPONSE_ADDED: 'Respondent added response',
+    RESPONSE_CANCELED: 'HR or Admin or Respondent canceled review assignment',
+    SYSTEM_AUTOMATED: 'Automated system transition',
+} as const;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/review-stage-transitions.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/review-stage-transitions.ts.html new file mode 100644 index 00000000..a0907bec --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/constants/review-stage-transitions.ts.html @@ -0,0 +1,256 @@ + + + + + + Code coverage report for feedback360/application/constants/review-stage-transitions.ts + + + + + + + + + +
+
+

All files / feedback360/application/constants review-stage-transitions.ts

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+ 
+/**
+ * State machine transition map for Review lifecycle
+ * Defines which stage transitions are allowed
+ */
+export const REVIEW_STAGE_TRANSITIONS: Record<ReviewStage, ReviewStage[]> = {
+    [ReviewStage.NEW]: [ReviewStage.SELF_ASSESSMENT, ReviewStage.CANCELED],
+ 
+    [ReviewStage.SELF_ASSESSMENT]: [
+        ReviewStage.WAITING_TO_START,
+        ReviewStage.IN_PROGRESS,
+        ReviewStage.CANCELED,
+    ],
+ 
+    [ReviewStage.WAITING_TO_START]: [
+        ReviewStage.IN_PROGRESS,
+        ReviewStage.FINISHED,
+        ReviewStage.CANCELED,
+    ],
+ 
+    [ReviewStage.IN_PROGRESS]: [ReviewStage.FINISHED, ReviewStage.CANCELED],
+ 
+    [ReviewStage.FINISHED]: [ReviewStage.PREPARING_REPORT],
+ 
+    [ReviewStage.PREPARING_REPORT]: [
+        ReviewStage.PROCESSING_BY_HR,
+        ReviewStage.PUBLISHED,
+        ReviewStage.ANALYSIS,
+        ReviewStage.ARCHIVED,
+    ],
+ 
+    [ReviewStage.PROCESSING_BY_HR]: [
+        ReviewStage.PUBLISHED,
+        ReviewStage.ANALYSIS,
+        ReviewStage.ARCHIVED,
+    ],
+ 
+    [ReviewStage.PUBLISHED]: [ReviewStage.ANALYSIS, ReviewStage.ARCHIVED],
+ 
+    [ReviewStage.ANALYSIS]: [ReviewStage.ARCHIVED],
+ 
+    [ReviewStage.CANCELED]: [ReviewStage.ARCHIVED],
+ 
+    [ReviewStage.ARCHIVED]: [],
+};
+ 
+/**
+ * Transition reasons for automated stage changes
+ */
+export const TRANSITION_REASONS = {
+    REVIEW_UPDATED: 'Review updated',
+    ALL_RESPONSES_COLLECTED: 'All responses collected from respondents',
+    DEADLINE_REACHED: 'Review deadline has been reached',
+    FORCE_FINISH: 'Manually finished by HR or Admin',
+    SYSTEM_AUTOMATED: 'Automated system transition',
+} as const;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/cycle-stage-changed.event.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/cycle-stage-changed.event.ts.html new file mode 100644 index 00000000..2480e96f --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/cycle-stage-changed.event.ts.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for feedback360/application/events/cycle-stage-changed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events cycle-stage-changed.event.ts

+
+ +
+ 100% + Statements + 7/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 7/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +12x +  +9x +9x +9x +9x +9x +9x +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+ 
+export class CycleStageChangedEvent {
+    constructor(
+        public readonly cycleId: number,
+        public readonly fromStage: CycleStage,
+        public readonly toStage: CycleStage,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/cycle-stage-processed.event.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/cycle-stage-processed.event.ts.html new file mode 100644 index 00000000..e9ed9cff --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/cycle-stage-processed.event.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for feedback360/application/events/cycle-stage-processed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events cycle-stage-processed.event.ts

+
+ +
+ 0% + Statements + 0/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+import { CycleStageChangedEvent } from './cycle-stage-changed.event';
+ 
+export class CycleStageProcessedEvent {
+    constructor(
+        public readonly cycleId: number,
+        public readonly currentStage: CycleStage,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+ 
+    static fromCycleStageChangedEvent(
+        event: CycleStageChangedEvent,
+    ): CycleStageProcessedEvent {
+        return new CycleStageProcessedEvent(
+            event.cycleId,
+            event.toStage,
+            event.changedById,
+            event.changedByName,
+            event.reason,
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/index.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/index.html new file mode 100644 index 00000000..380d6928 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/index.html @@ -0,0 +1,176 @@ + + + + + + Code coverage report for feedback360/application/events + + + + + + + + + +
+
+

All files feedback360/application/events

+
+ +
+ 19.44% + Statements + 7/36 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 14.28% + Functions + 1/7 +
+ + +
+ 19.44% + Lines + 7/36 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage-changed.event.ts +
+
100%7/7100%0/0100%1/1100%7/7
cycle-stage-processed.event.ts +
+
0%0/7100%0/00%0/20%0/7
respondent-status-changed.event.ts +
+
0%0/8100%0/00%0/10%0/8
review-stage-changed.event.ts +
+
0%0/7100%0/00%0/10%0/7
review-stage-processed.event.ts +
+
0%0/7100%0/00%0/20%0/7
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/respondent-status-changed.event.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/respondent-status-changed.event.ts.html new file mode 100644 index 00000000..e9a541b6 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/respondent-status-changed.event.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for feedback360/application/events/respondent-status-changed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events respondent-status-changed.event.ts

+
+ +
+ 0% + Statements + 0/8 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { ResponseStatus } from '@intra/shared-kernel';
+ 
+export class RespondentStatusChangedEvent {
+    constructor(
+        public readonly reviewId: number,
+        public readonly respondentRelationId: number,
+        public readonly fromStatus: ResponseStatus,
+        public readonly toStatus: ResponseStatus,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/review-stage-changed.event.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/review-stage-changed.event.ts.html new file mode 100644 index 00000000..951af3da --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/review-stage-changed.event.ts.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for feedback360/application/events/review-stage-changed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events review-stage-changed.event.ts

+
+ +
+ 0% + Statements + 0/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +  +  +  +  +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+ 
+export class ReviewStageChangedEvent {
+    constructor(
+        public readonly reviewId: number,
+        public readonly fromStage: ReviewStage,
+        public readonly toStage: ReviewStage,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/review-stage-processed.event.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/review-stage-processed.event.ts.html new file mode 100644 index 00000000..9cdf86ac --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/events/review-stage-processed.event.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for feedback360/application/events/review-stage-processed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events review-stage-processed.event.ts

+
+ +
+ 0% + Statements + 0/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+import { ReviewStageChangedEvent } from './review-stage-changed.event';
+ 
+export class ReviewStageProcessedEvent {
+    constructor(
+        public readonly reviewId: number,
+        public readonly currentStage: ReviewStage,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+ 
+    static fromReviewStageChangedEvent(
+        event: ReviewStageChangedEvent,
+    ): ReviewStageProcessedEvent {
+        return new ReviewStageProcessedEvent(
+            event.reviewId,
+            event.toStage,
+            event.changedById,
+            event.changedByName,
+            event.reason,
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/cycle-stage.listener.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/cycle-stage.listener.ts.html new file mode 100644 index 00000000..5af04851 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/cycle-stage.listener.ts.html @@ -0,0 +1,340 @@ + + + + + + Code coverage report for feedback360/application/listeners/cycle-stage.listener.ts + + + + + + + + + +
+
+

All files / feedback360/application/listeners cycle-stage.listener.ts

+
+ +
+ 96.66% + Statements + 29/30 +
+ + +
+ 75% + Branches + 18/24 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 96.42% + Lines + 27/28 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +8618x +18x +18x +18x +18x +18x +18x +  +  +  +  +  +  +  +  +  +  +18x +18x +  +  +18x +18x +  +18x +  +  +  +  +  +  +  +  +18x +  +  +14x +  +  +  +14x +  +8x +  +  +  +8x +  +  +6x +  +6x +2x +  +  +2x +  +  +  +2x +  +  +4x +  +  +  +4x +4x +  +4x +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+import { Inject, Injectable, Logger } from '@nestjs/common';
+import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
+import { CycleStageChangedEvent } from '../../../feedback360/application/events/cycle-stage-changed.event';
+import { ClusterScoreAnalyticsService } from '../../../feedback360/application/services/cluster-score-analytics.service';
+import { CycleStageProcessedEvent } from '../events/cycle-stage-processed.event';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+ 
+/**
+ * Event listener for cycle stage changes
+ * Automatically triggers cluster score analytics generation for the cycle
+ * when it transitions to the FINISHED stage
+ */
+@Injectable()
+export class CycleStageListener {
+    private readonly logger = new Logger(CycleStageListener.name);
+ 
+    constructor(
+        private readonly analytics: ClusterScoreAnalyticsService,
+        private readonly eventEmitter: EventEmitter2,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+    ) {}
+ 
+    /*
+     * Handles the cycle.stage.changed event
+     * Generates cluster score analytics for the cycle
+     * when it transitions to the FINISHED stage
+     */
+    @OnEvent('cycle.stage.changed')
+    async handleCycleStageChanged(
+        event: CycleStageChangedEvent,
+    ): Promise<void> {
+        this.logger.debug(
+            `Cycle ${event.cycleId} transitioned from ${event.fromStage} to ${event.toStage}`,
+        );
+ 
+        if (event.toStage !== CycleStage.FINISHED) {
+            // Emit event for listeners to react
+            this.eventEmitter.emit(
+                'cycle.stage.processed',
+                CycleStageProcessedEvent.fromCycleStageChangedEvent(event),
+            );
+            return;
+        }
+ 
+        const reviews = await this.reviews.listByCycleId(event.cycleId);
+ 
+        if (reviews.length === 0) {
+            this.logger.debug(
+                `No reviews found for cycle ${event.cycleId}. Skipping cluster score analytics generation.`,
+            );
+            this.eventEmitter.emit(
+                'cycle.stage.processed',
+                CycleStageProcessedEvent.fromCycleStageChangedEvent(event),
+            );
+            return;
+        }
+ 
+        this.logger.debug(
+            `Initiating cluster score analytics generation for cycle ${event.cycleId}`,
+        );
+ 
+        try {
+            await this.analytics.generateAnalyticsForCycle(event.cycleId);
+ 
+            this.logger.debug(
+                `Successfully generated cluster score analytics for cycle ${event.cycleId}`,
+            );
+        } catch (error) {
+            this.logger.error(
+                `Failed to generate cluster score analytics for cycle ${event.cycleId}: ${error.message}`,
+            );
+        } finally {
+            // Emit event for listeners to react
+            this.eventEmitter.emit(
+                'cycle.stage.processed',
+                CycleStageProcessedEvent.fromCycleStageChangedEvent(event),
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/index.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/index.html new file mode 100644 index 00000000..280152b9 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for feedback360/application/listeners + + + + + + + + + +
+
+

All files feedback360/application/listeners

+
+ +
+ 94.26% + Statements + 115/122 +
+ + +
+ 76.31% + Branches + 87/114 +
+ + +
+ 100% + Functions + 11/11 +
+ + +
+ 93.85% + Lines + 107/114 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage.listener.ts +
+
96.66%29/3075%18/24100%2/296.42%27/28
respondent-status.listener.ts +
+
92.45%49/5377.41%48/62100%6/691.83%45/49
review-stage.listener.ts +
+
94.87%37/3975%21/28100%3/394.59%35/37
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/respondent-status.listener.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/respondent-status.listener.ts.html new file mode 100644 index 00000000..9bb03a59 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/respondent-status.listener.ts.html @@ -0,0 +1,583 @@ + + + + + + Code coverage report for feedback360/application/listeners/respondent-status.listener.ts + + + + + + + + + +
+
+

All files / feedback360/application/listeners respondent-status.listener.ts

+
+ +
+ 92.45% + Statements + 49/53 +
+ + +
+ 77.41% + Branches + 48/62 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 91.83% + Lines + 45/49 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +16718x +  +  +  +  +18x +18x +18x +18x +18x +  +  +  +18x +  +  +  +18x +  +  +  +  +  +  +18x +18x +  +  +  +18x +18x +18x +  +  +  +  +  +  +  +  +18x +  +  +5x +  +  +  +5x +  +  +  +2x +  +  +3x +  +  +  +3x +3x +  +  +  +3x +  +4x +  +  +  +3x +2x +2x +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +18x +18x +  +  +  +18x +  +18x +18x +  +  +  +18x +  +  +6x +2x +  +  +4x +4x +  +  +  +4x +2x +  +  +2x +2x +  +  +  +  +  +  +2x +1x +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    RespondentCategory,
+    ResponseStatus,
+    ReviewStage,
+} from '@intra/shared-kernel';
+import { Inject, Injectable, Logger } from '@nestjs/common';
+import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
+import { TRANSITION_REASONS } from '../constants/review-stage-transitions';
+import { RespondentStatusChangedEvent } from '../events/respondent-status-changed.event';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../ports/respondent.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+import { ReviewService } from '../services/review.service';
+ 
+/**
+ * Event listener for respondent status changes
+ * Automatically triggers review completion when all responses are collected
+ */
+@Injectable()
+export class RespondentStatusListener {
+    private readonly logger = new Logger(RespondentStatusListener.name);
+ 
+    constructor(
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        private readonly reviews: ReviewService,
+        private readonly eventEmitter: EventEmitter2,
+    ) {}
+ 
+    /*
+     * Handles the respondent.status.changed event
+     * Changes review status to finished
+     * if all responses for the review are collected
+     */
+    @OnEvent('respondent.status.changed')
+    async handleRespondentStatusChanged(
+        event: RespondentStatusChangedEvent,
+    ): Promise<void> {
+        this.logger.debug(
+            `Review response ${event.respondentRelationId} transitioned from ${event.fromStatus} to ${event.toStatus}`,
+        );
+ 
+        if (
+            event.toStatus !== ResponseStatus.COMPLETED &&
+            event.toStatus !== ResponseStatus.CANCELED
+        ) {
+            return;
+        }
+ 
+        this.logger.debug(
+            `Checking if all responses for review ${event.reviewId} are collected`,
+        );
+ 
+        try {
+            const respondents = await this.respondents.listByReview(
+                event.reviewId,
+                {},
+            );
+            const allCompleted = respondents.every(
+                (respondent) =>
+                    respondent.responseStatus === ResponseStatus.COMPLETED ||
+                    respondent.responseStatus === ResponseStatus.CANCELED,
+            );
+ 
+            if (allCompleted) {
+                await this.reviews.completeReview(event.reviewId);
+                this.logger.debug(
+                    `Successfully completed review ${event.reviewId} due to all responses being collected`,
+                );
+            } else {
+                this.logger.debug(
+                    `Review ${event.reviewId} is not completed yet. Remaining responses: ${
+                        respondents.filter(
+                            (respondent) =>
+                                respondent.responseStatus ===
+                                    ResponseStatus.PENDING ||
+                                respondent.responseStatus ===
+                                    ResponseStatus.IN_PROGRESS,
+                        ).length
+                    }`,
+                );
+            }
+        } catch (error) {
+            this.logger.error(
+                `Failed to check response collecting completion for review ${event.reviewId}: ${error.message}`,
+                error.stack,
+            );
+        }
+    }
+}
+ 
+/**
+ * Event listener that auto-transitions a review from SELF_ASSESSMENT to
+ * IN_PROGRESS once the ratee has submitted their self-assessment response.
+ *
+ * Trigger: respondent.status.changed → toStatus = COMPLETED
+ *          AND respondent.category = SELF_ASSESSMENT
+ *          AND review.stage = SELF_ASSESSMENT.
+ */
+@Injectable()
+export class SelfAssessmentCompletedListener {
+    private readonly logger = new Logger(SelfAssessmentCompletedListener.name);
+ 
+    constructor(
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        private readonly reviewService: ReviewService,
+    ) {}
+ 
+    @OnEvent('respondent.status.changed')
+    async handleRespondentCompleted(
+        event: RespondentStatusChangedEvent,
+    ): Promise<void> {
+        if (event.toStatus !== ResponseStatus.COMPLETED) {
+            return;
+        }
+ 
+        try {
+            const respondent = await this.respondents.getById(
+                event.respondentRelationId,
+            );
+ 
+            if (respondent.category !== RespondentCategory.SELF_ASSESSMENT) {
+                return;
+            }
+ 
+            const review = await this.reviews.findById(event.reviewId);
+            Iif (!review) {
+                this.logger.warn(
+                    `Review ${event.reviewId} not found while handling self-assessment completion`,
+                );
+                return;
+            }
+ 
+            if (review.stage !== ReviewStage.SELF_ASSESSMENT) {
+                return;
+            }
+ 
+            this.logger.debug(
+                `Ratee submitted self-assessment for review ${event.reviewId}; advancing stage SELF_ASSESSMENT → IN_PROGRESS`,
+            );
+ 
+            await this.reviewService.changeReviewStage(
+                event.reviewId,
+                ReviewStage.IN_PROGRESS,
+                event.changedById ?? undefined,
+                event.changedByName ?? undefined,
+                TRANSITION_REASONS.SYSTEM_AUTOMATED,
+            );
+        } catch (error) {
+            this.logger.error(
+                `Failed to auto-advance review ${event.reviewId} to IN_PROGRESS after self-assessment completion: ${
+                    (error as Error).message
+                }`,
+                (error as Error).stack,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/review-stage.listener.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/review-stage.listener.ts.html new file mode 100644 index 00000000..e966882e --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/listeners/review-stage.listener.ts.html @@ -0,0 +1,460 @@ + + + + + + Code coverage report for feedback360/application/listeners/review-stage.listener.ts + + + + + + + + + +
+
+

All files / feedback360/application/listeners review-stage.listener.ts

+
+ +
+ 94.87% + Statements + 37/39 +
+ + +
+ 75% + Branches + 21/28 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 94.59% + Lines + 35/37 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +12618x +18x +18x +18x +18x +18x +18x +  +  +  +  +  +  +  +  +  +  +  +  +18x +  +  +  +  +  +  +  +  +  +  +  +18x +18x +  +  +18x +  +18x +18x +  +  +  +  +  +  +  +  +18x +  +  +11x +  +  +  +11x +11x +3x +  +  +8x +  +8x +  +  +  +  +8x +1x +  +  +1x +  +  +7x +  +  +  +7x +7x +  +  +  +  +  +  +  +  +  +  +  +  +6x +7x +  +6x +  +6x +5x +1x +  +  +  +1x +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +10x +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+import { Inject, Injectable, Logger } from '@nestjs/common';
+import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
+import { ReviewStageChangedEvent } from '../../../feedback360/application/events/review-stage-changed.event';
+import { CycleService } from '../../../feedback360/application/services/cycle.service';
+import { ReviewStageProcessedEvent } from '../events/review-stage-processed.event';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+ 
+/**
+ * Stages where a review is still actively collecting responses.
+ * A cycle is considered ready to complete when no review is in any of
+ * these stages — i.e. every review has moved past response collection,
+ * regardless of whether it is currently FINISHED, in the report
+ * generation pipeline (PREPARING_REPORT / PROCESSING_BY_HR), already
+ * PUBLISHED / ANALYSIS / ARCHIVED, or CANCELED.
+ */
+const ACTIVE_REVIEW_STAGES: readonly ReviewStage[] = [
+    ReviewStage.NEW,
+    ReviewStage.SELF_ASSESSMENT,
+    ReviewStage.WAITING_TO_START,
+    ReviewStage.IN_PROGRESS,
+];
+ 
+/**
+ * Event listener for review stage changes
+ * Automatically triggers cycle completion when all reviews are finished
+ */
+@Injectable()
+export class ReviewStageListener {
+    private readonly logger = new Logger(ReviewStageListener.name);
+ 
+    constructor(
+        private readonly cycles: CycleService,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        private readonly eventEmitter: EventEmitter2,
+    ) {}
+ 
+    /**
+     * Handles review.stage.changed events
+     * Changes cycle status to finished
+     * if all reviews for the cycle are finished
+     */
+    @OnEvent('review.stage.changed')
+    async handleReviewStageChanged(
+        event: ReviewStageChangedEvent,
+    ): Promise<void> {
+        this.logger.debug(
+            `Review ${event.reviewId} transitioned from ${event.fromStage} to ${event.toStage}`,
+        );
+ 
+        try {
+            if (event.toStage !== ReviewStage.FINISHED) {
+                return;
+            }
+ 
+            const review = await this.reviews.findById(event.reviewId);
+ 
+            Iif (!review) {
+                this.logger.warn(`Review ${event.reviewId} not found`);
+                return;
+            }
+ 
+            if (!review.cycleId) {
+                this.logger.debug(
+                    `Review ${event.reviewId} has no cycle attached; skipping cycle completion check`,
+                );
+                return;
+            }
+ 
+            this.logger.debug(
+                `Check if all reviews for cycle ${review.cycleId} are finished`,
+            );
+ 
+            try {
+                const cycleReviews = await this.reviews.listByCycleId(
+                    review.cycleId,
+                );
+                // A cycle is "completable" once no review is still in an
+                // active (response-collection) stage. We treat anything
+                // past IN_PROGRESS — including PREPARING_REPORT,
+                // PROCESSING_BY_HR, PUBLISHED, ANALYSIS, ARCHIVED, and
+                // CANCELED — as already done from the cycle's POV. This
+                // matters because individual reviews are auto-progressed
+                // through the report pipeline as soon as they reach
+                // FINISHED, so by the time the LAST review in the cycle
+                // hits FINISHED, all earlier ones are already in
+                // PROCESSING_BY_HR or further.
+                const remainingActive = cycleReviews.filter((cycleReview) =>
+                    ACTIVE_REVIEW_STAGES.includes(cycleReview.stage),
+                );
+                const allCompleted = remainingActive.length === 0;
+ 
+                if (allCompleted) {
+                    await this.cycles.completeCycle(review.cycleId);
+                    this.logger.debug(
+                        `Successfully completed cycle ${review.cycleId} due to all reviews being completed`,
+                    );
+                } else {
+                    this.logger.debug(
+                        `Cycle ${review.cycleId} is not completed yet. Remaining reviews: ${remainingActive.length}`,
+                    );
+                }
+            } catch (error) {
+                this.logger.error(
+                    `Failed to check review collecting completion for cycle ${review.cycleId}: ${error.message}`,
+                    error.stack,
+                );
+            }
+        } finally {
+            // Always notify downstream listeners (e.g. report generation)
+            // regardless of whether the review had a cycle attached or
+            // whether the cycle completion check succeeded. Skipping this
+            // emit would prevent individual report generation for reviews
+            // that aren't part of a cycle.
+            this.eventEmitter.emit(
+                'review.stage.processed',
+                ReviewStageProcessedEvent.fromReviewStageChangedEvent(event),
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/answer.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/answer.repository.port.ts.html new file mode 100644 index 00000000..962fb25f --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/answer.repository.port.ts.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for feedback360/application/ports/answer.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports answer.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +  +  +19x +  +  +  +  +  +  +  +  +  +  + 
import { AnswerSearchQuery, RespondentCategory } from '@intra/shared-kernel';
+import { AnswerDomain } from '../../domain/answer.domain';
+ 
+export const ANSWER_REPOSITORY = Symbol('FEEDBACK360.ANSWER_REPOSITORY');
+ 
+export interface AnswerRepositoryPort {
+    create(answer: AnswerDomain): Promise<AnswerDomain>;
+    list(query: AnswerSearchQuery): Promise<AnswerDomain[]>;
+    findById(id: number): Promise<AnswerDomain | null>;
+    getAnswersCountByRespondentCategories(
+        reviewId: number,
+    ): Promise<{ respondentCategory: RespondentCategory; answers: number }[]>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cluster-score-analytics.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cluster-score-analytics.repository.port.ts.html new file mode 100644 index 00000000..25b27ace --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cluster-score-analytics.repository.port.ts.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for feedback360/application/ports/cluster-score-analytics.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports cluster-score-analytics.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26  +  +  +  +  +  +12x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    ClusterScoreAnalyticsSearchQuery,
+    UpdateClusterScoreAnalyticsPayload,
+} from '@intra/shared-kernel';
+import { ClusterScoreAnalyticsDomain } from '../../domain/cluster-score-analytics.domain';
+ 
+export const CLUSTER_SCORE_ANALYTICS_REPOSITORY = Symbol(
+    'FEEDBACK360.CLUSTER_SCORE_ANALYTICS_REPOSITORY',
+);
+ 
+export interface ClusterScoreAnalyticsRepositoryPort {
+    upsert(
+        analytics: ClusterScoreAnalyticsDomain,
+    ): Promise<ClusterScoreAnalyticsDomain>;
+    findById(id: number): Promise<ClusterScoreAnalyticsDomain | null>;
+    search(
+        query: ClusterScoreAnalyticsSearchQuery,
+    ): Promise<ClusterScoreAnalyticsDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdateClusterScoreAnalyticsPayload,
+    ): Promise<ClusterScoreAnalyticsDomain>;
+    deleteById(id: number): Promise<void>;
+    getByCycleId(cycleId: number): Promise<ClusterScoreAnalyticsDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cluster-score.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cluster-score.repository.port.ts.html new file mode 100644 index 00000000..9f9fc209 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cluster-score.repository.port.ts.html @@ -0,0 +1,130 @@ + + + + + + Code coverage report for feedback360/application/ports/cluster-score.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports cluster-score.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16  +  +  +  +12x +  +  +  +  +  +  +  +  +  +  + 
import { ClusterScoreSearchQuery } from '@intra/shared-kernel';
+import { ClusterScoreWithRelationsDomain } from '../../domain/cluster-score-with-relations.domain';
+import { ClusterScoreDomain } from '../../domain/cluster-score.domain';
+ 
+export const CLUSTER_SCORE_REPOSITORY = Symbol(
+    'FEEDBACK360.CLUSTER_SCORE_REPOSITORY',
+);
+ 
+export interface ClusterScoreRepositoryPort {
+    upsert(score: ClusterScoreDomain): Promise<ClusterScoreDomain>;
+    list(query: ClusterScoreSearchQuery): Promise<ClusterScoreDomain[]>;
+    getById(id: number): Promise<ClusterScoreWithRelationsDomain>;
+    deleteById(id: number): Promise<void>;
+    getByCycleId(cycleId: number): Promise<ClusterScoreWithRelationsDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cycle-stage-history.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cycle-stage-history.repository.port.ts.html new file mode 100644 index 00000000..a3916118 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cycle-stage-history.repository.port.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for feedback360/application/ports/cycle-stage-history.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports cycle-stage-history.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +12x +  +  +  +  +  +  +  +  + 
import { CycleStageHistoryDomain } from '../../domain/cycle-stage-history.domain';
+ 
+export const CYCLE_STAGE_HISTORY_REPOSITORY = Symbol(
+    'FEEDBACK360.CYCLE_STAGE_HISTORY_REPOSITORY',
+);
+ 
+export interface CycleStageHistoryRepositoryPort {
+    create(history: CycleStageHistoryDomain): Promise<CycleStageHistoryDomain>;
+    findByCycleId(cycleId: number): Promise<CycleStageHistoryDomain[]>;
+    findById(id: number): Promise<CycleStageHistoryDomain | null>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cycle.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cycle.repository.port.ts.html new file mode 100644 index 00000000..70b498e6 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/cycle.repository.port.ts.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for feedback360/application/ports/cycle.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports cycle.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +  +20x +  +  +  +  +  +  +  +  + 
import { CycleSearchQuery, UpdateCyclePayload } from '@intra/shared-kernel';
+import { CycleDomain } from '../../domain/cycle.domain';
+ 
+export const CYCLE_REPOSITORY = Symbol('FEEDBACK360.CYCLE_REPOSITORY');
+ 
+export interface CycleRepositoryPort {
+    create(cycle: CycleDomain): Promise<CycleDomain>;
+    findById(id: number): Promise<CycleDomain | null>;
+    search(query: CycleSearchQuery): Promise<CycleDomain[]>;
+    updateById(id: number, patch: UpdateCyclePayload): Promise<CycleDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/index.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/index.html new file mode 100644 index 00000000..d22b1a3f --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/index.html @@ -0,0 +1,266 @@ + + + + + + Code coverage report for feedback360/application/ports + + + + + + + + + +
+
+

All files feedback360/application/ports

+
+ +
+ 100% + Statements + 11/11 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 11/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
answer.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
cluster-score-analytics.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
cluster-score.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
cycle-stage-history.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
cycle.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
question.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
respondent.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
review-question-relation.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
review-stage-history.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
review.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
reviewer.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/question.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/question.repository.port.ts.html new file mode 100644 index 00000000..46cb8ffa --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/question.repository.port.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for feedback360/application/ports/question.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports question.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +28x +  +  +  +  +  +  +  + 
import { QuestionSearchQuery } from '@intra/shared-kernel';
+import { QuestionDomain } from '../../domain/question.domain';
+ 
+export const QUESTION_REPOSITORY = Symbol('FEEDBACK360.QUESTION_REPOSITORY');
+ 
+export interface QuestionRepositoryPort {
+    create(question: QuestionDomain): Promise<QuestionDomain>;
+    findById(id: number): Promise<QuestionDomain | null>;
+    search(query: QuestionSearchQuery): Promise<QuestionDomain[]>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/respondent.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/respondent.repository.port.ts.html new file mode 100644 index 00000000..3e5f5659 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/respondent.repository.port.ts.html @@ -0,0 +1,154 @@ + + + + + + Code coverage report for feedback360/application/ports/respondent.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports respondent.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24  +  +  +  +  +  +19x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    RespondentSearchQuery,
+    UpdateRespondentPayload,
+} from '@intra/shared-kernel';
+import { RespondentDomain } from '../../domain/respondent.domain';
+ 
+export const RESPONDENT_REPOSITORY = Symbol(
+    'FEEDBACK360.RESPONDENT_REPOSITORY',
+);
+ 
+export interface RespondentRepositoryPort {
+    create(relation: RespondentDomain): Promise<RespondentDomain>;
+    getById(id: number): Promise<RespondentDomain>;
+    listByReview(
+        reviewId: number,
+        query: RespondentSearchQuery,
+    ): Promise<RespondentDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdateRespondentPayload,
+    ): Promise<RespondentDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review-question-relation.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review-question-relation.repository.port.ts.html new file mode 100644 index 00000000..27db5cad --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review-question-relation.repository.port.ts.html @@ -0,0 +1,136 @@ + + + + + + Code coverage report for feedback360/application/ports/review-question-relation.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports review-question-relation.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18  +  +  +12x +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { ReviewQuestionRelationSearchQuery } from '@intra/shared-kernel';
+import { ReviewQuestionRelationDomain } from '../../domain/review-question-relation.domain';
+ 
+export const REVIEW_QUESTION_RELATION_REPOSITORY = Symbol(
+    'FEEDBACK360.REVIEW_QUESTION_RELATION_REPOSITORY',
+);
+ 
+export interface ReviewQuestionRelationRepositoryPort {
+    link(
+        relation: ReviewQuestionRelationDomain,
+    ): Promise<ReviewQuestionRelationDomain>;
+    listByReview(
+        reviewId: number,
+        query: ReviewQuestionRelationSearchQuery,
+    ): Promise<ReviewQuestionRelationDomain[]>;
+    unlink(reviewId: number, questionId: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review-stage-history.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review-stage-history.repository.port.ts.html new file mode 100644 index 00000000..07b1179e --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review-stage-history.repository.port.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for feedback360/application/ports/review-stage-history.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports review-stage-history.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +12x +  +  +  +  +  +  +  +  +  +  + 
import { ReviewStageHistoryDomain } from '../../domain/review-stage-history.domain';
+ 
+export const REVIEW_STAGE_HISTORY_REPOSITORY = Symbol(
+    'FEEDBACK360.REVIEW_STAGE_HISTORY_REPOSITORY',
+);
+ 
+export interface ReviewStageHistoryRepositoryPort {
+    create(
+        history: ReviewStageHistoryDomain,
+    ): Promise<ReviewStageHistoryDomain>;
+    findByReviewId(reviewId: number): Promise<ReviewStageHistoryDomain[]>;
+    findById(id: number): Promise<ReviewStageHistoryDomain | null>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review.repository.port.ts.html new file mode 100644 index 00000000..e3a38c63 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/review.repository.port.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for feedback360/application/ports/review.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports review.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +  +20x +  +  +  +  +  +  +  +  +  + 
import { ReviewSearchQuery, UpdateReviewPayload } from '@intra/shared-kernel';
+import { ReviewDomain } from '../../domain/review.domain';
+ 
+export const REVIEW_REPOSITORY = Symbol('FEEDBACK360.REVIEW_REPOSITORY');
+ 
+export interface ReviewRepositoryPort {
+    create(review: ReviewDomain): Promise<ReviewDomain>;
+    findById(id: number): Promise<ReviewDomain | null>;
+    search(query: ReviewSearchQuery): Promise<ReviewDomain[]>;
+    updateById(id: number, patch: UpdateReviewPayload): Promise<ReviewDomain>;
+    deleteById(id: number): Promise<void>;
+    listByCycleId(cycleId: number): Promise<ReviewDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/reviewer.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/reviewer.repository.port.ts.html new file mode 100644 index 00000000..1d65299d --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/ports/reviewer.repository.port.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for feedback360/application/ports/reviewer.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports reviewer.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +  +19x +  +  +  +  +  +  +  +  +  + 
import { ReviewerSearchQuery } from '@intra/shared-kernel';
+import { ReviewerDomain } from '../../domain/reviewer.domain';
+ 
+export const REVIEWER_REPOSITORY = Symbol('FEEDBACK360.REVIEWER_REPOSITORY');
+ 
+export interface ReviewerRepositoryPort {
+    create(relation: ReviewerDomain): Promise<ReviewerDomain>;
+    listByReview(
+        reviewId: number,
+        query: ReviewerSearchQuery,
+    ): Promise<ReviewerDomain[]>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/cluster-score-analytics.service.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/cluster-score-analytics.service.ts.html new file mode 100644 index 00000000..761930f7 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/cluster-score-analytics.service.ts.html @@ -0,0 +1,838 @@ + + + + + + Code coverage report for feedback360/application/services/cluster-score-analytics.service.ts + + + + + + + + + +
+
+

All files / feedback360/application/services cluster-score-analytics.service.ts

+
+ +
+ 96.25% + Statements + 77/80 +
+ + +
+ 68% + Branches + 34/50 +
+ + +
+ 100% + Functions + 17/17 +
+ + +
+ 96.15% + Lines + 75/78 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +25235x +  +  +  +  +  +35x +  +  +  +  +  +35x +35x +35x +35x +  +  +  +35x +  +  +  +35x +  +  +35x +  +  +35x +  +35x +35x +35x +  +  +  +  +  +13x +12x +12x +12x +12x +12x +  +12x +11x +  +9x +  +  +  +  +  +  +  +  +  +  +  +9x +  +  +  +  +  +1x +  +  +  +6x +6x +2x +  +  +4x +  +  +  +  +  +  +2x +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +1x +1x +  +  +  +  +  +6x +  +  +  +  +  +  +  +7x +  +6x +6x +  +  +  +6x +3x +  +  +  +3x +3x +  +  +3x +1x +  +  +2x +3x +  +2x +2x +2x +3x +  +  +2x +  +2x +2x +2x +2x +  +3x +3x +  +2x +  +  +  +2x +2x +  +2x +2x +  +2x +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +15x +1x +  +  +  +14x +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +14x +  +  +  +  +14x +  +  +  +  +14x +1x +  +  +  +  +  + 
import {
+    CLUSTER_SCORE_ANALYTICS_CONSTRAINTS,
+    ClusterScoreAnalyticsSearchQuery,
+    UpdateClusterScoreAnalyticsPayload,
+    UpsertClusterScoreAnalyticsPayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import Decimal from 'decimal.js';
+import { ClusterService } from '../../../library/application/services/cluster.service';
+import { ClusterScoreAnalyticsDomain } from '../../domain/cluster-score-analytics.domain';
+import {
+    CLUSTER_SCORE_ANALYTICS_REPOSITORY,
+    ClusterScoreAnalyticsRepositoryPort,
+} from '../ports/cluster-score-analytics.repository.port';
+import {
+    CLUSTER_SCORE_REPOSITORY,
+    ClusterScoreRepositoryPort,
+} from '../ports/cluster-score.repository.port';
+import { CycleService } from './cycle.service';
+ 
+@Injectable()
+export class ClusterScoreAnalyticsService {
+    constructor(
+        @Inject(CLUSTER_SCORE_ANALYTICS_REPOSITORY)
+        private readonly analytics: ClusterScoreAnalyticsRepositoryPort,
+        @Inject(CLUSTER_SCORE_REPOSITORY)
+        private readonly clusterScores: ClusterScoreRepositoryPort,
+        private readonly cycles: CycleService,
+        private readonly clusters: ClusterService,
+    ) {}
+ 
+    async upsert(
+        payload: UpsertClusterScoreAnalyticsPayload,
+    ): Promise<ClusterScoreAnalyticsDomain> {
+        await this.cycles.getById(payload.cycleId);
+        const minScore = new Decimal(payload.minScore);
+        const maxScore = new Decimal(payload.maxScore);
+        const averageScore = new Decimal(payload.averageScore);
+        const lowerBound = new Decimal(payload.lowerBound);
+        const upperBound = new Decimal(payload.upperBound);
+ 
+        await this.validateBounds(lowerBound, upperBound);
+        await this.validateScores(minScore, maxScore, averageScore);
+ 
+        const domain = ClusterScoreAnalyticsDomain.create({
+            cycleId: payload.cycleId,
+            clusterId: payload.clusterId,
+            employeesCount: payload.employeesCount,
+            employeeDensity: payload.employeeDensity,
+            lowerBound,
+            upperBound,
+            minScore,
+            maxScore,
+            averageScore,
+        });
+ 
+        return this.analytics.upsert(domain);
+    }
+ 
+    async search(
+        query: ClusterScoreAnalyticsSearchQuery,
+    ): Promise<ClusterScoreAnalyticsDomain[]> {
+        return this.analytics.search(query);
+    }
+ 
+    async getById(id: number): Promise<ClusterScoreAnalyticsDomain> {
+        const item = await this.analytics.findById(id);
+        if (!item)
+            throw new NotFoundException(
+                'Cycle cluster analytics with id ' + id + ' not found',
+            );
+        return item;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateClusterScoreAnalyticsPayload,
+    ): Promise<ClusterScoreAnalyticsDomain> {
+        const current = await this.getById(id);
+ 
+        const minScore =
+            patch.minScore !== undefined
+                ? new Decimal(patch.minScore)
+                : current.minScore;
+        const maxScore =
+            patch.maxScore !== undefined
+                ? new Decimal(patch.maxScore)
+                : current.maxScore;
+        const averageScore =
+            patch.averageScore !== undefined
+                ? new Decimal(patch.averageScore)
+                : current.averageScore;
+ 
+        await this.validateScores(minScore, maxScore, averageScore);
+ 
+        const payload: UpdateClusterScoreAnalyticsPayload = {
+            ...(patch.employeesCount !== undefined
+                ? { employeesCount: patch.employeesCount }
+                : {}),
+            ...(patch.employeeDensity !== undefined
+                ? { employeeDensity: patch.employeeDensity }
+                : {}),
+            ...(patch.minScore !== undefined
+                ? { minScore: patch.minScore }
+                : {}),
+            ...(patch.maxScore !== undefined
+                ? { maxScore: patch.maxScore }
+                : {}),
+            ...(patch.averageScore !== undefined
+                ? { averageScore: patch.averageScore }
+                : {}),
+        };
+ 
+        return this.analytics.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        await this.analytics.deleteById(id);
+    }
+ 
+    async getByCycleId(
+        cycleId: number,
+    ): Promise<ClusterScoreAnalyticsDomain[]> {
+        return this.analytics.getByCycleId(cycleId);
+    }
+ 
+    /**
+     * Generates analytics metrics for a cycle.
+     * @param cycleId The cycle identifier.
+     */
+    async generateAnalyticsForCycle(cycleId: number): Promise<void> {
+        await this.cycles.getById(cycleId);
+ 
+        const clusters = await this.clusters.search({});
+        const clusterScores = await this.clusterScores.list({
+            cycleId,
+        });
+ 
+        for (const cluster of clusters) {
+            Iif (!cluster.id) {
+                continue;
+            }
+ 
+            const currentClusterScores = clusterScores.filter(
+                (cs) => cs.clusterId === cluster.id,
+            );
+ 
+            if (!currentClusterScores.length) {
+                continue;
+            }
+ 
+            const scores = currentClusterScores.map(
+                (cs) => new Decimal(cs.score),
+            );
+            const minScore = Decimal.min(...scores).toDecimalPlaces(4);
+            const maxScore = Decimal.max(...scores).toDecimalPlaces(4);
+            const averageScore = scores
+                .reduce((a, b) => a.plus(b), new Decimal(0))
+                .dividedBy(scores.length)
+                .toDecimalPlaces(4);
+            const employeesCount = currentClusterScores.length;
+ 
+            const competenceClusterIds = clusters
+                .filter((c) => c.competenceId === cluster.competenceId)
+                .map((c) => c.id);
+            const rateesByCompetence = new Set(
+                clusterScores
+                    .filter((cs) => competenceClusterIds.includes(cs.clusterId))
+                    .map((cs) => cs.rateeId),
+            );
+            const employeeDensity = new Decimal(currentClusterScores.length)
+                .dividedBy(rateesByCompetence.size)
+                .toDecimalPlaces(4);
+ 
+            const lowerBound = new Decimal(cluster.lowerBound);
+            const upperBound = new Decimal(cluster.upperBound);
+ 
+            await this.validateBounds(lowerBound, upperBound);
+            await this.validateScores(minScore, maxScore, averageScore);
+ 
+            const domain = ClusterScoreAnalyticsDomain.create({
+                cycleId,
+                clusterId: cluster.id,
+                employeesCount,
+                employeeDensity,
+                lowerBound,
+                upperBound,
+                minScore,
+                maxScore,
+                averageScore,
+            });
+ 
+            await this.analytics.upsert(domain);
+        }
+    }
+ 
+    /**
+     * Validates the scores for a cluster.
+     * @param min The minimum score.
+     * @param max The maximum score.
+     * @param avg The average score.
+     */
+    private async validateScores(
+        min: Decimal,
+        max: Decimal,
+        avg: Decimal,
+    ): Promise<void> {
+        if (min.gt(max)) {
+            throw new BadRequestException(
+                'Min score must be less than or equal to max score',
+            );
+        }
+        if (avg.lt(min) || avg.gt(max)) {
+            throw new BadRequestException(
+                'Average score must be between min and max scores',
+            );
+        }
+    }
+ 
+    /**
+     * Validates the bounds for a cluster.
+     * @param lower The lower bound.
+     * @param upper The upper bound.
+     */
+    private async validateBounds(
+        lower: Decimal,
+        upper: Decimal,
+    ): Promise<void> {
+        Iif (lower.lt(CLUSTER_SCORE_ANALYTICS_CONSTRAINTS.SCORE.MIN)) {
+            throw new BadRequestException(
+                'Lower bound must be greater than or equal to min score',
+            );
+        }
+        Iif (upper.gt(CLUSTER_SCORE_ANALYTICS_CONSTRAINTS.SCORE.MAX)) {
+            throw new BadRequestException(
+                'Upper bound must be less than or equal to max score',
+            );
+        }
+        if (lower.gt(upper)) {
+            throw new BadRequestException(
+                'Lower bound must be less than or equal to upper bound',
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/cycle.service.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/cycle.service.ts.html new file mode 100644 index 00000000..4510d678 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/cycle.service.ts.html @@ -0,0 +1,1024 @@ + + + + + + Code coverage report for feedback360/application/services/cycle.service.ts + + + + + + + + + +
+
+

All files / feedback360/application/services cycle.service.ts

+
+ +
+ 97.18% + Statements + 69/71 +
+ + +
+ 75.28% + Branches + 67/89 +
+ + +
+ 100% + Functions + 13/13 +
+ + +
+ 97.1% + Lines + 67/69 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +31438x +  +  +  +  +  +  +  +  +38x +  +  +  +  +  +38x +38x +  +  +  +38x +38x +38x +38x +  +  +  +38x +38x +  +  +  +38x +  +  +  +38x +  +  +  +  +  +38x +  +  +37x +  +37x +  +37x +37x +37x +  +37x +  +  +  +60x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +60x +60x +  +  +  +7x +  +  +  +151x +151x +9x +142x +  +  +  +5x +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +4x +  +  +  +  +  +  +  +  +4x +  +4x +  +3x +3x +1x +  +  +  +  +  +3x +  +3x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +2x +2x +  +  +  +  +  +  +  +  +  +3x +  +  +  +2x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +16x +15x +  +  +15x +  +15x +2x +  +  +  +  +  +13x +  +13x +  +  +  +  +  +13x +  +  +  +  +  +  +  +  +13x +  +  +  +13x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +8x +  +  +6x +  +  +6x +  +5x +  +  +6x +  +  +6x +4x +  +  +  +  +  +  +2x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +  +  +  +  +  +  +  +  + 
import {
+    CreateCyclePayload,
+    CYCLE_CONSTRAINTS,
+    CycleSearchQuery,
+    CycleStage,
+    ReviewStage,
+    SYSTEM_ACTOR,
+    UpdateCyclePayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { EventEmitter2 } from '@nestjs/event-emitter';
+import {
+    STRATEGIC_REPORT_REPOSITORY,
+    StrategicReportRepositoryPort,
+} from 'src/contexts/reporting/application/ports/strategic-report.repository.port';
+import { PrismaService } from 'src/database/prisma.service';
+import { CycleStageHistoryDomain } from '../../domain/cycle-stage-history.domain';
+import { CycleDomain } from '../../domain/cycle.domain';
+import {
+    CYCLE_STAGE_TRANSITIONS,
+    TRANSITION_REASONS,
+} from '../constants/cycle-stage-transitions';
+import { CycleStageChangedEvent } from '../events/cycle-stage-changed.event';
+import {
+    CYCLE_STAGE_HISTORY_REPOSITORY,
+    CycleStageHistoryRepositoryPort,
+} from '../ports/cycle-stage-history.repository.port';
+import {
+    CYCLE_REPOSITORY,
+    CycleRepositoryPort,
+} from '../ports/cycle.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+ 
+@Injectable()
+export class CycleService {
+    constructor(
+        @Inject(CYCLE_REPOSITORY)
+        private readonly cycles: CycleRepositoryPort,
+        @Inject(CYCLE_STAGE_HISTORY_REPOSITORY)
+        private readonly stageHistory: CycleStageHistoryRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        private readonly eventEmitter: EventEmitter2,
+        private readonly prisma: PrismaService,
+        @Inject(STRATEGIC_REPORT_REPOSITORY)
+        private readonly strategicReports: StrategicReportRepositoryPort,
+    ) {}
+ 
+    async create(payload: CreateCyclePayload): Promise<CycleDomain> {
+        const cycle = CycleDomain.create({
+            title: payload.title,
+            description: payload.description,
+            hrId: payload.hrId,
+            stage: payload.stage ?? CycleStage.NEW,
+            minRespondentsThreshold:
+                payload.minRespondentsThreshold ??
+                CYCLE_CONSTRAINTS.ANONYMITY_THRESHOLD.MIN,
+            isActive: payload.isActive ?? true,
+            startDate: payload.startDate,
+            reviewDeadline: payload.reviewDeadline,
+            approvalDeadline: payload.approvalDeadline,
+            responseDeadline: payload.responseDeadline,
+            endDate: payload.endDate,
+        });
+ 
+        const created = await this.cycles.create(cycle);
+        return this.getById(created.id!);
+    }
+ 
+    async search(query: CycleSearchQuery): Promise<CycleDomain[]> {
+        return this.cycles.search(query);
+    }
+ 
+    async getById(id: number): Promise<CycleDomain> {
+        const cycle = await this.cycles.findById(id);
+        if (!cycle)
+            throw new NotFoundException('Cycle with id ' + id + ' not found');
+        return cycle;
+    }
+ 
+    async update(id: number, patch: UpdateCyclePayload): Promise<CycleDomain> {
+        const cycle = await this.getById(id);
+ 
+        const payload: UpdateCyclePayload = {
+            ...(patch.title !== undefined ? { title: patch.title } : {}),
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+            ...(patch.hrId !== undefined ? { hrId: patch.hrId } : {}),
+            ...(patch.isActive !== undefined
+                ? { isActive: patch.isActive }
+                : {}),
+            ...(patch.startDate !== undefined
+                ? { startDate: patch.startDate }
+                : {}),
+            ...(patch.reviewDeadline !== undefined
+                ? { reviewDeadline: patch.reviewDeadline }
+                : {}),
+            ...(patch.approvalDeadline !== undefined
+                ? { approvalDeadline: patch.approvalDeadline }
+                : {}),
+            ...(patch.responseDeadline !== undefined
+                ? { responseDeadline: patch.responseDeadline }
+                : {}),
+            ...(patch.endDate !== undefined ? { endDate: patch.endDate } : {}),
+        };
+ 
+        // Check if cycle has strategic report and title is being updated
+        const strategicReport = await this.strategicReports.findByCycleId(id);
+ 
+        Iif (strategicReport !== null && payload.title) {
+            throw new BadRequestException(
+                'Cycle ' +
+                    cycle.id +
+                    ' has strategic report. Cannot update. Current stage: ' +
+                    cycle.stage,
+            );
+        }
+ 
+        const updated = await this.cycles.updateById(id, payload);
+ 
+        if (patch.stage && (patch.stage as CycleStage) !== cycle.stage) {
+            // Check if cycle has incomplete reviews and stage is being updated to a final stage
+            const reviews = await this.reviews.listByCycleId(id);
+            const incompleteReviews = reviews.filter((r) =>
+                [
+                    ReviewStage.SELF_ASSESSMENT,
+                    ReviewStage.IN_PROGRESS,
+                    ReviewStage.PROCESSING_BY_HR,
+                ].includes(r.stage),
+            );
+            const hasInProgressReviews = incompleteReviews.length > 0;
+ 
+            if (
+                hasInProgressReviews &&
+                patch.stage &&
+                [
+                    CycleStage.CANCELED,
+                    CycleStage.FINISHED,
+                    CycleStage.PREPARING_REPORT,
+                    CycleStage.PUBLISHED,
+                    CycleStage.ARCHIVED,
+                ].includes(patch.stage)
+            ) {
+                throw new BadRequestException(
+                    'Cycle ' +
+                        cycle.id +
+                        ' has ' +
+                        incompleteReviews.length +
+                        ' active or processing by HR reviews. Cannot update. Current cycle stage: ' +
+                        cycle.stage,
+                );
+            }
+ 
+            // Reactive Trigger: Check if all responses completed
+            Iif (patch.stage && patch.stage === CycleStage.FINISHED) {
+                await this.completeCycle(id);
+            }
+ 
+            Eif (patch.stage && patch.stage !== CycleStage.PUBLISHED) {
+                await this.changeStage(
+                    id,
+                    patch.stage,
+                    SYSTEM_ACTOR.ID,
+                    SYSTEM_ACTOR.NAME,
+                    TRANSITION_REASONS.CYCLE_UPDATED,
+                );
+            }
+        }
+ 
+        return updated;
+    }
+ 
+    async delete(id: number): Promise<void> {
+        const cycle = await this.getById(id);
+ 
+        await this.cycles.deleteById(id);
+    }
+ 
+    /**
+     * Centralized method for stage transitions with validation and history tracking
+     * @param cycleId ID of the cycle
+     * @param nextStage Target stage
+     * @param actorId User ID who initiated the change (0 for system)
+     * @param actorName Full name of the actor
+     * @param reason Optional reason for the transition
+     */
+    async changeStage(
+        cycleId: number,
+        nextStage: CycleStage,
+        actorId: number = SYSTEM_ACTOR.ID,
+        actorName: string | null = SYSTEM_ACTOR.NAME,
+        reason?: string,
+    ): Promise<void> {
+        const cycle = await this.getById(cycleId);
+        const currentStage = cycle.stage;
+ 
+        // Validate transition is allowed
+        const allowedTransitions = CYCLE_STAGE_TRANSITIONS[currentStage];
+ 
+        if (!allowedTransitions.includes(nextStage)) {
+            throw new BadRequestException(
+                `Invalid cycle stage transition from ${currentStage} to ${nextStage}`,
+            );
+        }
+ 
+        // Perform update and history logging in a transaction
+        await this.prisma.$transaction(async (tx) => {
+            // Update cycle stage
+            await tx.cycle.update({
+                where: { id: cycleId },
+                data: { stage: nextStage },
+            });
+ 
+            // Create history record
+            const history = CycleStageHistoryDomain.create({
+                cycleId,
+                fromStage: currentStage,
+                toStage: nextStage,
+                changedById: actorId || null,
+                changedByName: actorName,
+                reason,
+            });
+ 
+            await this.stageHistory.create(history);
+        });
+ 
+        // Emit event for listeners to react
+        this.eventEmitter.emit(
+            'cycle.stage.changed',
+            new CycleStageChangedEvent(
+                cycleId,
+                currentStage,
+                nextStage,
+                actorId,
+                actorName,
+                reason,
+            ),
+        );
+    }
+ 
+    /**
+     * Get stage change history for a cycle
+     */
+    async getStageHistory(cycleId: number): Promise<CycleStageHistoryDomain[]> {
+        return this.stageHistory.findByCycleId(cycleId);
+    }
+ 
+    /**
+     * REACTIVE TRIGGER: Checks if all review for a cycle have collected.
+     * If yes, automatically transitions the cycle to the FINISHED stage.
+     * @param cycleId The cycle identifier.
+     */
+    async completeCycle(cycleId: number): Promise<void> {
+        const cycle = await this.getById(cycleId);
+ 
+        // Get all reviews for this cycle
+        const reviews = await this.reviews.listByCycleId(cycleId);
+ 
+        // Check if there are any in-progress reviews
+        const incompleteReviews = reviews.filter(
+            (r) =>
+                r.stage === ReviewStage.SELF_ASSESSMENT ||
+                r.stage === ReviewStage.IN_PROGRESS,
+        );
+        const hasInProgressReviews = incompleteReviews.length > 0;
+ 
+        // If no in-progress reviews, trigger report generation
+        if (!hasInProgressReviews && reviews.length > 0) {
+            await this.changeStage(
+                cycleId,
+                CycleStage.FINISHED,
+                SYSTEM_ACTOR.ID,
+                SYSTEM_ACTOR.NAME,
+                TRANSITION_REASONS.ALL_REVIEWS_COLLECTED,
+            );
+        } else if (hasInProgressReviews && reviews.length > 0) {
+            throw new BadRequestException(
+                'All reviews must be completed to finish cycle #' +
+                    cycleId +
+                    '. Incomplete reviews count: ' +
+                    incompleteReviews.length,
+            );
+        }
+    }
+ 
+    /**
+     * MANUAL TRIGGER: HR or ADMIN force-finishes a cycle
+     * Transitions cycle to FINISHED regardless of in-progress reviews
+     */
+    async forceFinish(
+        cycleId: number,
+        actorId: number = SYSTEM_ACTOR.ID,
+        actorName: string | null = SYSTEM_ACTOR.NAME,
+    ): Promise<void> {
+        await this.getById(cycleId);
+        await this.changeStage(
+            cycleId,
+            CycleStage.FINISHED,
+            actorId,
+            actorName,
+            TRANSITION_REASONS.FORCE_FINISH,
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/index.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/index.html new file mode 100644 index 00000000..ae9c4bd7 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for feedback360/application/services + + + + + + + + + +
+
+

All files feedback360/application/services

+
+ +
+ 78.69% + Statements + 362/460 +
+ + +
+ 65.02% + Branches + 290/446 +
+ + +
+ 82.66% + Functions + 62/75 +
+ + +
+ 81.35% + Lines + 349/429 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cluster-score-analytics.service.ts +
+
96.25%77/8068%34/50100%17/1796.15%75/78
cycle.service.ts +
+
97.18%69/7175.28%67/89100%13/1397.1%67/69
review-scheduler.service.ts +
+
91.48%43/4785.71%24/28100%3/391.11%41/45
review.service.ts +
+
66.03%173/26259.13%165/27969.04%29/4270.04%166/237
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/review-scheduler.service.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/review-scheduler.service.ts.html new file mode 100644 index 00000000..09c0bc38 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/review-scheduler.service.ts.html @@ -0,0 +1,541 @@ + + + + + + Code coverage report for feedback360/application/services/review-scheduler.service.ts + + + + + + + + + +
+
+

All files / feedback360/application/services review-scheduler.service.ts

+
+ +
+ 91.48% + Statements + 43/47 +
+ + +
+ 85.71% + Branches + 24/28 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 91.11% + Lines + 41/45 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +15318x +  +  +  +  +  +18x +18x +18x +18x +18x +18x +  +  +  +  +  +18x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +18x +18x +  +  +18x +18x +  +  +  +  +  +  +  +18x +4x +  +4x +  +4x +  +  +  +4x +4x +1x +  +  +3x +3x +  +  +3x +  +2x +  +  +  +  +  +  +  +2x +  +  +  +  +2x +1x +1x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +18x +2x +  +2x +2x +  +  +  +  +2x +  +2x +  +2x +  +2x +  +  +2x +1x +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    CycleStage,
+    IdentityRole,
+    ReviewStage,
+    SYSTEM_ACTOR,
+} from '@intra/shared-kernel';
+import { Injectable, Logger } from '@nestjs/common';
+import { Cron, CronExpression } from '@nestjs/schedule';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { TRANSITION_REASONS } from '../constants/review-stage-transitions';
+import { CycleService } from './cycle.service';
+import { ReviewService } from './review.service';
+ 
+/**
+ * Synthetic actor used by the scheduler when calling access-checked
+ * methods on `ReviewService`
+ */
+const SYSTEM_USER = UserDomain.create({
+    id: SYSTEM_ACTOR.ID,
+    firstName: SYSTEM_ACTOR.NAME,
+    lastName: '',
+    email: 'system@intra.local',
+    roles: [IdentityRole.ADMIN],
+});
+ 
+/**
+ * Scheduled service for checking review deadlines.
+ * Runs periodically to transition reviews to FINISHED once their cycle's
+ * `responseDeadline` has passed. The review state machine only allows
+ * IN_PROGRESS → FINISHED | CANCELED — moving directly to PREPARING_REPORT
+ * is not a valid transition, so the deadline trigger lands the review in
+ * FINISHED and the report-generation flow takes it from there.
+ */
+@Injectable()
+export class ReviewSchedulerService {
+    private readonly logger = new Logger(ReviewSchedulerService.name);
+ 
+    constructor(
+        private readonly reviewService: ReviewService,
+        private readonly cycleService: CycleService,
+    ) {}
+ 
+    /**
+     * SCHEDULED TRIGGER: Check for reviews past their deadline
+     * Runs every day at 7am to find reviews that should be completed
+     */
+    @Cron(CronExpression.EVERY_DAY_AT_7AM)
+    async checkReviewDeadlines(): Promise<void> {
+        this.logger.log('Checking for reviews past deadline...');
+ 
+        try {
+            // Get all active cycles
+            const cycles = await this.cycleService.search({
+                isActive: true,
+            });
+ 
+            for (const cycle of cycles) {
+                if (!cycle.responseDeadline) {
+                    continue;
+                }
+ 
+                const now = new Date();
+                const deadline = new Date(cycle.responseDeadline);
+ 
+                // If deadline has passed
+                if (now > deadline) {
+                    // Find all reviews in this cycle that are still IN_PROGRESS.
+                    const reviews = await this.reviewService.search(
+                        {
+                            cycleId: cycle.id!,
+                            stage: ReviewStage.IN_PROGRESS,
+                        },
+                        SYSTEM_USER,
+                    );
+ 
+                    this.logger.log(
+                        `Found ${reviews.length} reviews past deadline in cycle #${cycle.id}`,
+                    );
+ 
+                    // Transition each review to FINISHED
+                    for (const review of reviews) {
+                        try {
+                            await this.reviewService.changeReviewStage(
+                                review.id!,
+                                ReviewStage.FINISHED,
+                                SYSTEM_ACTOR.ID,
+                                SYSTEM_ACTOR.NAME,
+                                TRANSITION_REASONS.DEADLINE_REACHED,
+                            );
+                            this.logger.log(
+                                `Transitioned review #${review.id} to FINISHED (deadline)`,
+                            );
+                        } catch (error) {
+                            this.logger.error(
+                                `Failed to transition review #${review.id}: ${error.message}`,
+                            );
+                        }
+                    }
+                }
+            }
+        } catch (error) {
+            this.logger.error(
+                `Error checking review deadlines: ${error.message}`,
+            );
+        }
+    }
+ 
+    /**
+     * SCHEDULED TRIGGER: Finish cycles past their deadlines
+     * Runs every day at 7am to find cycles that should be finished
+     */
+    @Cron(CronExpression.EVERY_DAY_AT_7AM)
+    async checkCycleDeadlines(): Promise<void> {
+        this.logger.log('Checking for cycles past deadline...');
+ 
+        try {
+            const cycles = await this.cycleService.search({
+                stage: CycleStage.ACTIVE,
+                isActive: true,
+            });
+ 
+            const now = new Date();
+ 
+            for (const cycle of cycles) {
+                const endDatePassed =
+                    cycle.endDate && new Date(cycle.endDate) < now;
+                const responseDeadlinePassed =
+                    cycle.responseDeadline &&
+                    new Date(cycle.responseDeadline) < now;
+ 
+                if (!endDatePassed && !responseDeadlinePassed) {
+                    continue;
+                }
+ 
+                try {
+                    await this.cycleService.forceFinish(cycle.id!);
+                    this.logger.log(
+                        `Finished cycle #${cycle.id} (deadline reached)`,
+                    );
+                } catch (error) {
+                    this.logger.error(
+                        `Failed to finish cycle #${cycle.id}: ${error.message}`,
+                    );
+                }
+            }
+        } catch (error) {
+            this.logger.error(
+                `Error checking cycle deadlines: ${error.message}`,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/review.service.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/review.service.ts.html new file mode 100644 index 00000000..027a7aef --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/application/services/review.service.ts.html @@ -0,0 +1,3160 @@ + + + + + + Code coverage report for feedback360/application/services/review.service.ts + + + + + + + + + +
+
+

All files / feedback360/application/services review.service.ts

+
+ +
+ 66.03% + Statements + 173/262 +
+ + +
+ 59.13% + Branches + 165/279 +
+ + +
+ 69.04% + Functions + 29/42 +
+ + +
+ 70.04% + Lines + 166/237 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +102635x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +35x +  +  +  +  +  +  +35x +  +35x +35x +35x +35x +  +35x +35x +35x +35x +35x +35x +35x +35x +  +  +  +35x +  +  +  +35x +35x +35x +  +  +  +35x +  +  +  +35x +  +  +  +35x +  +  +  +35x +  +  +  +35x +  +  +  +35x +  +  +  +35x +  +  +  +35x +  +  +35x +  +  +35x +  +35x +  +35x +  +35x +  +35x +  +35x +  +35x +  +35x +35x +35x +35x +35x +35x +  +  +  +16x +16x +  +  +15x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +15x +15x +  +  +  +  +  +  +4x +  +4x +  +  +3x +  +  +  +3x +  +  +3x +  +4x +  +  +  +62x +62x +1x +  +61x +  +60x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +3x +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +61x +  +  +4x +  +  +4x +  +2x +2x +  +2x +  +1x +  +  +  +1x +  +1x +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +2x +  +2x +  +  +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +2x +  +2x +2x +1x +  +  +  +  +  +  +1x +  +  +  +  +  +7x +7x +7x +  +  +  +  +  +  +  +  +  +  +  +  +7x +  +  +  +7x +7x +  +7x +  +  +  +  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +7x +  +  +  +  +  +  +  +2x +  +2x +  +  +  +  +  +2x +2x +2x +  +  +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +3x +  +3x +1x +  +  +  +  +  +  +  +2x +  +  +2x +  +  +  +  +  +  +2x +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +2x +2x +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +2x +  +2x +  +  +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +5x +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +3x +3x +  +  +  +  +3x +  +  +3x +3x +1x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +1x +  +  +  +  +  +1x +  +  +  +  +  +  +  +2x +2x +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +4x +4x +  +  +4x +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +10x +10x +  +  +10x +  +10x +1x +  +  +  +  +  +9x +  +9x +  +  +  +  +  +9x +  +  +  +  +  +  +  +  +9x +  +  +  +9x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +4x +  +  +4x +  +  +4x +  +4x +  +  +4x +  +  +4x +3x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  + 
import {
+    AddQuestionToReviewPayload,
+    AddRespondentPayload,
+    AddReviewerPayload,
+    ClusterScoreSearchQuery,
+    CreateAnswerPayload,
+    CreateQuestionPayload,
+    CreateReviewPayload,
+    CycleStage,
+    IdentityRole,
+    QuestionSearchQuery,
+    RespondentCategory,
+    RespondentSearchQuery,
+    ResponseStatus,
+    ReviewerSearchQuery,
+    ReviewQuestionRelationSearchQuery,
+    ReviewSearchQuery,
+    ReviewStage,
+    SYSTEM_ACTOR,
+    UpdateRespondentPayload,
+    UpdateReviewPayload,
+    UpsertClusterScorePayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    ForbiddenException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { EventEmitter2 } from '@nestjs/event-emitter';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { CompetenceService } from 'src/contexts/library/application/services/competence.service';
+import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service';
+import { PrismaService } from 'src/database/prisma.service';
+import { AnswerDomain } from '../../domain/answer.domain';
+import { ClusterScoreWithRelationsDomain } from '../../domain/cluster-score-with-relations.domain';
+import { ClusterScoreDomain } from '../../domain/cluster-score.domain';
+import { QuestionDomain } from '../../domain/question.domain';
+import { RespondentDomain } from '../../domain/respondent.domain';
+import { ReviewQuestionRelationDomain } from '../../domain/review-question-relation.domain';
+import { ReviewStageHistoryDomain } from '../../domain/review-stage-history.domain';
+import { ReviewDomain } from '../../domain/review.domain';
+import { ReviewerDomain } from '../../domain/reviewer.domain';
+import {
+    RESPONSE_STATUS_TRANSITIONS,
+    TRANSITION_REASONS as RESPONSE_TRANSITION_REASONS,
+} from '../constants/response-status-transitions';
+import {
+    REVIEW_STAGE_TRANSITIONS,
+    TRANSITION_REASONS as REVIEW_TRANSITION_REASONS,
+} from '../constants/review-stage-transitions';
+import { RespondentStatusChangedEvent } from '../events/respondent-status-changed.event';
+import { ReviewStageChangedEvent } from '../events/review-stage-changed.event';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../ports/answer.repository.port';
+import {
+    CLUSTER_SCORE_REPOSITORY,
+    ClusterScoreRepositoryPort,
+} from '../ports/cluster-score.repository.port';
+import {
+    QuestionRepositoryPort,
+    QUESTION_REPOSITORY as REVIEW_QUESTION_REPOSITORY,
+} from '../ports/question.repository.port';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../ports/respondent.repository.port';
+import {
+    REVIEW_QUESTION_RELATION_REPOSITORY,
+    ReviewQuestionRelationRepositoryPort,
+} from '../ports/review-question-relation.repository.port';
+import {
+    REVIEW_STAGE_HISTORY_REPOSITORY,
+    ReviewStageHistoryRepositoryPort,
+} from '../ports/review-stage-history.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from '../ports/reviewer.repository.port';
+import { CycleService } from './cycle.service';
+ 
+@Injectable()
+export class ReviewService {
+    constructor(
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        @Inject(REVIEW_QUESTION_REPOSITORY)
+        private readonly questions: QuestionRepositoryPort,
+        @Inject(REVIEW_QUESTION_RELATION_REPOSITORY)
+        private readonly questionRelations: ReviewQuestionRelationRepositoryPort,
+        @Inject(ANSWER_REPOSITORY)
+        private readonly answers: AnswerRepositoryPort,
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        @Inject(REVIEWER_REPOSITORY)
+        private readonly reviewers: ReviewerRepositoryPort,
+        @Inject(CLUSTER_SCORE_REPOSITORY)
+        private readonly clusterScores: ClusterScoreRepositoryPort,
+        @Inject(REVIEW_STAGE_HISTORY_REPOSITORY)
+        private readonly stageHistory: ReviewStageHistoryRepositoryPort,
+        private readonly prisma: PrismaService,
+        private readonly questionTemplates: QuestionTemplateService,
+        private readonly competences: CompetenceService,
+        private readonly cycles: CycleService,
+        private readonly eventEmitter: EventEmitter2,
+    ) {}
+ 
+    async create(payload: CreateReviewPayload): Promise<ReviewDomain> {
+        Eif (payload.cycleId) {
+            await this.cycles.getById(payload.cycleId);
+        }
+ 
+        const review = ReviewDomain.create({
+            rateeId: payload.rateeId,
+            rateeFullName: payload.rateeFullName,
+            rateePositionId: payload.rateePositionId,
+            rateePositionTitle: payload.rateePositionTitle,
+            hrId: payload.hrId,
+            hrFullName: payload.hrFullName,
+            hrNote: payload.hrNote,
+            teamId: payload.teamId ?? null,
+            teamTitle: payload.teamTitle ?? null,
+            managerId: payload.managerId ?? null,
+            managerFullName: payload.managerFullName ?? null,
+            managerPositionId: payload.managerPositionId ?? null,
+            managerPositionTitle: payload.managerPositionTitle ?? null,
+            cycleId: payload.cycleId ?? null,
+            stage: payload.stage ?? ReviewStage.NEW,
+            reportId: payload.reportId ?? null,
+        });
+ 
+        const created = await this.reviews.create(review);
+        return this.getById(created.id!);
+    }
+ 
+    async search(
+        query: ReviewSearchQuery,
+        actor?: UserDomain,
+    ): Promise<ReviewDomain[]> {
+        const reviews = await this.reviews.search(query);
+ 
+        const filteredReviews = (
+            await Promise.all(
+                reviews.map(async (review) => {
+                    const hasAccessToReview = await this.hasAccessToReadReview(
+                        review,
+                        actor,
+                    );
+                    return hasAccessToReview ? review : undefined;
+                }),
+            )
+        ).filter((review) => review !== undefined);
+ 
+        return filteredReviews;
+    }
+ 
+    async getById(id: number, actor?: UserDomain): Promise<ReviewDomain> {
+        const review = await this.reviews.findById(id);
+        if (!review)
+            throw new NotFoundException('Review with id ' + id + ' not found');
+ 
+        await this.checkAccessToReview(review, actor);
+ 
+        return review;
+    }
+ 
+    /**
+     * Checks if the actor has access to the review answers
+     * as an admin, HR, manager, ratee, or reviewer.
+     * @param review The review to check access for.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToReviewAnswers(
+        review: ReviewDomain,
+        actor?: UserDomain,
+    ): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        if (reviewers.length > 0) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view review #' + review.id + '.',
+        );
+    }
+ 
+    private async hasAccessToReadReview(
+        review: ReviewDomain,
+        actor?: UserDomain,
+    ): Promise<boolean> {
+        if (!actor) return false;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        Eif (isAdminOrHr) return true;
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return true;
+ 
+        const respondents = await this.respondents.listByReview(review.id!, {
+            respondentId: actor.id,
+        });
+ 
+        if (respondents.length > 0) return true;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        if (reviewers.length > 0) return true;
+ 
+        return false;
+    }
+ 
+    /**
+     * Checks if the actor has access to the review
+     * as an admin, HR, manager, ratee, respondent, or reviewer.
+     * @param review The review to check access for.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToReview(
+        review: ReviewDomain,
+        actor?: UserDomain,
+    ): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return;
+ 
+        const respondents = await this.respondents.listByReview(review.id!, {
+            respondentId: actor.id,
+        });
+ 
+        Iif (respondents.length > 0) return;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        Iif (reviewers.length > 0) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view review #' + review.id + '.',
+        );
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateReviewPayload,
+        actor: UserDomain,
+    ): Promise<ReviewDomain> {
+        const review = await this.getById(id);
+ 
+        if (
+            review.stage !== ReviewStage.NEW &&
+            review.stage !== ReviewStage.PROCESSING_BY_HR
+        ) {
+            throw new BadRequestException(
+                'Review #' +
+                    id +
+                    ' must be new or processing by HR to be updated. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        Iif (patch.cycleId) {
+            const cycle = await this.cycles.getById(patch.cycleId);
+            if (
+                cycle.stage !== CycleStage.NEW &&
+                cycle.stage !== CycleStage.ACTIVE
+            ) {
+                throw new BadRequestException(
+                    'Cycle #' +
+                        id +
+                        ' must be new or active to be assigned to review. Current stage: ' +
+                        cycle.stage,
+                );
+            }
+        }
+ 
+        const payload: UpdateReviewPayload = {
+            ...(patch.rateeId !== undefined ? { rateeId: patch.rateeId } : {}),
+            ...(patch.rateeFullName !== undefined
+                ? { rateeFullName: patch.rateeFullName }
+                : {}),
+            ...(patch.rateePositionId !== undefined
+                ? { rateePositionId: patch.rateePositionId }
+                : {}),
+            ...(patch.rateePositionTitle !== undefined
+                ? { rateePositionTitle: patch.rateePositionTitle }
+                : {}),
+            ...(patch.hrId !== undefined ? { hrId: patch.hrId } : {}),
+            ...(patch.hrFullName !== undefined
+                ? { hrFullName: patch.hrFullName }
+                : {}),
+            ...(patch.hrNote !== undefined ? { hrNote: patch.hrNote } : {}),
+            ...(patch.teamId !== undefined ? { teamId: patch.teamId } : {}),
+            ...(patch.teamTitle !== undefined
+                ? { teamTitle: patch.teamTitle }
+                : {}),
+            ...(patch.managerId !== undefined
+                ? { managerId: patch.managerId }
+                : {}),
+            ...(patch.managerFullName !== undefined
+                ? { managerFullName: patch.managerFullName }
+                : {}),
+            ...(patch.managerPositionId !== undefined
+                ? { managerPositionId: patch.managerPositionId }
+                : {}),
+            ...(patch.managerPositionTitle !== undefined
+                ? { managerPositionTitle: patch.managerPositionTitle }
+                : {}),
+            ...(patch.cycleId !== undefined ? { cycleId: patch.cycleId } : {}),
+            ...(patch.reportId !== undefined
+                ? { reportId: patch.reportId }
+                : {}),
+        };
+ 
+        const updatedReview = await this.reviews.updateById(id, payload);
+ 
+        // Reactive Trigger: Check if all responses completed
+        Iif (
+            patch.stage &&
+            patch.stage !== updatedReview.stage &&
+            patch.stage === ReviewStage.FINISHED
+        ) {
+            await this.completeReview(id);
+        }
+ 
+        Iif (
+            patch.stage &&
+            patch.stage !== updatedReview.stage &&
+            patch.stage !== ReviewStage.FINISHED
+        ) {
+            await this.changeReviewStage(
+                id,
+                patch.stage,
+                actor.id,
+                actor.fullName,
+                REVIEW_TRANSITION_REASONS.REVIEW_UPDATED,
+            );
+        }
+ 
+        return updatedReview;
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+ 
+        const existingAnswers = await this.answers.list({ reviewId: id });
+        if (existingAnswers.length > 0) {
+            throw new BadRequestException(
+                'Review #' +
+                    id +
+                    ' cannot be deleted. It already has answers submitted by respondents.',
+            );
+        }
+ 
+        await this.reviews.deleteById(id);
+    }
+ 
+    async createQuestion(
+        payload: CreateQuestionPayload,
+    ): Promise<QuestionDomain> {
+        Eif (payload.cycleId) {
+            const cycle = await this.cycles.getById(payload.cycleId);
+            Iif (
+                cycle.stage !== CycleStage.NEW &&
+                cycle.stage !== CycleStage.ACTIVE
+            ) {
+                throw new BadRequestException(
+                    'Cycle #' +
+                        cycle.id +
+                        ' must be in NEW or ACTIVE stage to be assigned to question. Current stage: ' +
+                        cycle.stage,
+                );
+            }
+        }
+ 
+        const baseQuestion = payload.questionTemplateId
+            ? await this.questionTemplates.getById(payload.questionTemplateId)
+            : null;
+ 
+        const title = payload.title ?? baseQuestion?.title;
+        const answerType = payload.answerType ?? baseQuestion?.answerType;
+ 
+        Iif (!title || !answerType) {
+            throw new NotFoundException(
+                'Base question data is required to create review question: title and answer type are required.',
+            );
+        }
+ 
+        const question = QuestionDomain.create({
+            cycleId: payload.cycleId ?? null,
+            questionTemplateId: payload.questionTemplateId ?? null,
+            title,
+            answerType,
+            competenceId:
+                payload.competenceId ?? baseQuestion?.competenceId ?? null,
+            isForSelfassessment:
+                payload.isForSelfassessment ??
+                baseQuestion?.isForSelfassessment ??
+                false,
+        });
+ 
+        return this.questions.create(question);
+    }
+ 
+    async listQuestions(query: QuestionSearchQuery): Promise<QuestionDomain[]> {
+        return this.questions.search(query);
+    }
+ 
+    async deleteQuestion(id: number): Promise<void> {
+        const question = await this.questions.findById(id);
+ 
+        Iif (!question) {
+            throw new NotFoundException(
+                'Question with id ' + id + ' not found',
+            );
+        }
+ 
+        Eif (question.cycleId) {
+            const cycle = await this.cycles.getById(question.cycleId);
+            if (
+                cycle.stage !== CycleStage.NEW &&
+                cycle.stage !== CycleStage.ACTIVE
+            ) {
+                throw new BadRequestException(
+                    'Question #' +
+                        id +
+                        ' cannot be deleted from cycle that is not active. Current stage: ' +
+                        cycle.stage,
+                );
+            }
+        }
+ 
+        await this.questions.deleteById(id);
+    }
+ 
+    async attachQuestion(
+        payload: AddQuestionToReviewPayload,
+    ): Promise<ReviewQuestionRelationDomain> {
+        const review = await this.getById(payload.reviewId);
+ 
+        if (review.stage !== ReviewStage.NEW) {
+            throw new BadRequestException(
+                'Review #' +
+                    review.id +
+                    ' must be not started to be assigned to question. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        const questionTemplate = await this.questionTemplates.getById(
+            payload.questionTemplateId,
+        );
+        const competence = await this.competences.getById(
+            questionTemplate.competenceId,
+        );
+ 
+        // The relation table FK requires a real `feedback360_questions` row.
+        // Reuse an existing snapshot for this (cycle, template) tuple if present,
+        // otherwise create a new feedback360 Question from the library template.
+        const existingQuestions = await this.questions.search({
+            cycleId: review.cycleId ?? undefined,
+            questionTemplateId: questionTemplate.id!,
+        });
+        const reviewQuestion =
+            existingQuestions[0] ??
+            (await this.questions.create(
+                QuestionDomain.create({
+                    cycleId: review.cycleId ?? null,
+                    questionTemplateId: questionTemplate.id!,
+                    title: questionTemplate.title,
+                    answerType: questionTemplate.answerType,
+                    competenceId: questionTemplate.competenceId,
+                    isForSelfassessment: questionTemplate.isForSelfassessment,
+                }),
+            ));
+ 
+        const relation = ReviewQuestionRelationDomain.create({
+            reviewId: payload.reviewId,
+            questionId: reviewQuestion.id!,
+            questionTitle: questionTemplate.title,
+            answerType: questionTemplate.answerType,
+            competenceId: questionTemplate.competenceId,
+            competenceTitle: competence.title,
+            competenceCode: competence.code,
+            competenceDescription: competence.description,
+            isForSelfassessment: questionTemplate.isForSelfassessment,
+        });
+ 
+        return this.questionRelations.link(relation);
+    }
+ 
+    async listQuestionRelations(
+        reviewId: number,
+        query: ReviewQuestionRelationSearchQuery,
+        actor?: UserDomain,
+    ): Promise<ReviewQuestionRelationDomain[]> {
+        await this.getById(reviewId, actor);
+        return this.questionRelations.listByReview(reviewId, query);
+    }
+ 
+    async detachQuestion(reviewId: number, questionId: number): Promise<void> {
+        const review = await this.getById(reviewId);
+ 
+        Iif (review.stage !== ReviewStage.NEW) {
+            throw new BadRequestException(
+                'Question #' +
+                    questionId +
+                    ' cannot be unassigned from review that is already started. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        await this.questionRelations.unlink(reviewId, questionId);
+    }
+ 
+    async addAnswer(
+        payload: CreateAnswerPayload,
+        actor: UserDomain,
+    ): Promise<AnswerDomain> {
+        const review = await this.getById(payload.reviewId, actor);
+ 
+        if (
+            review.stage !== ReviewStage.IN_PROGRESS &&
+            review.stage !== ReviewStage.SELF_ASSESSMENT
+        ) {
+            throw new BadRequestException(
+                'Review #' +
+                    review.id +
+                    ' must be in progress to add answer. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        const answer = AnswerDomain.create({
+            reviewId: payload.reviewId,
+            questionId: payload.questionId,
+            respondentCategory: payload.respondentCategory,
+            answerType: payload.answerType,
+            numericalValue: payload.numericalValue ?? null,
+            textValue: payload.textValue ?? null,
+        });
+ 
+        return await this.answers.create(answer);
+    }
+ 
+    async listAnswers(
+        reviewId: number,
+        respondentCategory?: RespondentCategory,
+        actor?: UserDomain,
+    ): Promise<AnswerDomain[]> {
+        const review = await this.getById(reviewId);
+        await this.checkAccessToReviewAnswers(review, actor);
+ 
+        return this.answers.list({ reviewId: reviewId, respondentCategory });
+    }
+ 
+    async countReviewAnswers(
+        reviewId: number,
+        actor?: UserDomain,
+    ): Promise<number> {
+        const review = await this.getById(reviewId);
+        await this.checkAccessToReviewAnswers(review, actor);
+ 
+        const counts =
+            await this.answers.getAnswersCountByRespondentCategories(reviewId);
+ 
+        return counts.reduce((acc, c) => acc + c.answers, 0);
+    }
+ 
+    async addRespondent(
+        payload: AddRespondentPayload,
+    ): Promise<RespondentDomain> {
+        const review = await this.getById(payload.reviewId);
+ 
+        if (
+            review.stage !== ReviewStage.NEW &&
+            review.stage !== ReviewStage.IN_PROGRESS &&
+            review.stage !== ReviewStage.WAITING_TO_START &&
+            review.stage !== ReviewStage.SELF_ASSESSMENT
+        ) {
+            throw new BadRequestException(
+                'Respondent ' +
+                    payload.fullName +
+                    ' cannot be added to review #' +
+                    review.id +
+                    ' that is already finished or cancelled. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        const relation = RespondentDomain.create({
+            reviewId: payload.reviewId,
+            respondentId: payload.respondentId,
+            fullName: payload.fullName,
+            category: payload.category,
+            responseStatus: payload.responseStatus,
+            respondentNote: payload.respondentNote,
+            hrNote: payload.hrNote,
+            positionId: payload.positionId,
+            positionTitle: payload.positionTitle,
+            teamId: payload.teamId,
+            teamTitle: payload.teamTitle,
+            invitedAt: payload.invitedAt,
+            canceledAt: payload.canceledAt,
+            respondedAt: payload.respondedAt,
+        });
+ 
+        return this.respondents.create(relation);
+    }
+ 
+    async updateRespondent(
+        relationId: number,
+        patch: UpdateRespondentPayload,
+        actor: UserDomain,
+    ): Promise<RespondentDomain> {
+        await this.checkAccessToUpdateResponseStatus(relationId, actor);
+ 
+        const relation = await this.respondents.getById(relationId);
+        const review = await this.getById(relation.reviewId);
+ 
+        if (
+            review.stage !== ReviewStage.IN_PROGRESS &&
+            review.stage !== ReviewStage.SELF_ASSESSMENT
+        ) {
+            throw new BadRequestException(
+                'Review #' +
+                    review.id +
+                    ' must be in progress to update response status. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        const responseStatus = patch.responseStatus;
+        patch.responseStatus = undefined;
+ 
+        const updated = await this.respondents.updateById(relationId, patch);
+ 
+        if (responseStatus) {
+            await this.changeRespondentStatus(
+                review.id!,
+                relationId,
+                responseStatus,
+                actor.id!,
+                actor.fullName,
+                RESPONSE_TRANSITION_REASONS.SYSTEM_AUTOMATED,
+            );
+        }
+ 
+        return updated;
+    }
+ 
+    /**
+     * Centralized method for response status transitions with validation
+     * @param reviewId ID of the review
+     * @param nextStatus Target status
+     * @param actorId User ID who initiated the change (0 for system)
+     * @param actorName Full name of the actor
+     * @param reason Optional reason for the transition
+     */
+    async changeRespondentStatus(
+        reviewId: number,
+        relationId: number,
+        nextStatus: ResponseStatus,
+        actorId: number = SYSTEM_ACTOR.ID,
+        actorName: string = SYSTEM_ACTOR.NAME,
+        reason: string = RESPONSE_TRANSITION_REASONS.SYSTEM_AUTOMATED,
+    ): Promise<void> {
+        const review = await this.getById(reviewId);
+        const respondent = await this.respondents.getById(relationId);
+        Iif (respondent.reviewId != reviewId) {
+            throw new BadRequestException(
+                `Respondent relation #${relationId} does not belong to review #${reviewId}`,
+            );
+        }
+        const currentStatus = respondent.responseStatus;
+ 
+        // Validate transition is allowed
+        const allowedTransitions = RESPONSE_STATUS_TRANSITIONS[currentStatus];
+        if (!allowedTransitions.includes(nextStatus)) {
+            throw new BadRequestException(
+                `Invalid response status transition from ${currentStatus} to ${nextStatus}`,
+            );
+        }
+ 
+        await this.respondents.updateById(relationId, {
+            responseStatus: nextStatus,
+        });
+ 
+        this.eventEmitter.emit(
+            'respondent.status.changed',
+            new RespondentStatusChangedEvent(
+                reviewId,
+                relationId,
+                currentStatus,
+                nextStatus,
+                actorId,
+                actorName,
+                reason,
+            ),
+        );
+    }
+ 
+    /**
+     * Checks if the actor has access to update the response status of a respondent
+     * as an admin or the respondent themselves.
+     * @param relationId The respondent identifier.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToUpdateResponseStatus(
+        relationId: number,
+        actor?: UserDomain,
+    ): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdmin = actor?.roles?.includes(IdentityRole.ADMIN);
+ 
+        if (isAdmin) return;
+ 
+        const respondent = await this.respondents.getById(relationId);
+ 
+        if (respondent.respondentId === actor.id) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to update response status for review #' +
+                respondent.reviewId +
+                '.',
+        );
+    }
+ 
+    async listRespondents(
+        reviewId: number,
+        query: RespondentSearchQuery,
+        actor?: UserDomain,
+    ): Promise<RespondentDomain[]> {
+        const review = await this.getById(reviewId);
+ 
+        Iif (!this.hasAccessToReadReview(review, actor)) {
+            throw new ForbiddenException(
+                'You do not have permission to view review #' + review.id + '.',
+            );
+        }
+ 
+        return this.respondents.listByReview(reviewId, query);
+    }
+ 
+    async removeRespondent(id: number): Promise<void> {
+        await this.respondents.deleteById(id);
+    }
+ 
+    async addReviewer(payload: AddReviewerPayload): Promise<ReviewerDomain> {
+        await this.getById(payload.reviewId);
+        const relation = ReviewerDomain.create({
+            reviewId: payload.reviewId,
+            reviewerId: payload.reviewerId,
+            fullName: payload.fullName,
+            positionId: payload.positionId,
+            positionTitle: payload.positionTitle,
+            teamId: payload.teamId,
+            teamTitle: payload.teamTitle,
+        });
+        return this.reviewers.create(relation);
+    }
+ 
+    async listReviewers(
+        reviewId: number,
+        query: ReviewerSearchQuery,
+        actor: UserDomain,
+    ): Promise<ReviewerDomain[]> {
+        await this.checkAccessToReviewers(reviewId, actor);
+        return this.reviewers.listByReview(reviewId, query);
+    }
+ 
+    /**
+     * Checks if the actor has access to the reviewers of the review
+     * as an admin, hr, manager, ratee, respondent, or reviewer of that review.
+     * @param reviewId The review identifier.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToReviewers(
+        reviewId: number,
+        actor?: UserDomain,
+    ): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const review = await this.getById(reviewId);
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return;
+ 
+        const respondents = await this.respondents.listByReview(review.id!, {
+            respondentId: actor.id,
+        });
+ 
+        if (respondents.length > 0) return;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        if (reviewers.length > 0) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view reviewers of review #' +
+                review.id +
+                '.',
+        );
+    }
+ 
+    async removeReviewer(id: number): Promise<void> {
+        await this.reviewers.deleteById(id);
+    }
+ 
+    async upsertClusterScore(
+        payload: UpsertClusterScorePayload,
+    ): Promise<ClusterScoreDomain> {
+        Eif (payload.cycleId) {
+            await this.cycles.getById(payload.cycleId);
+        }
+ 
+        const score = ClusterScoreDomain.create({
+            cycleId: payload.cycleId ?? null,
+            clusterId: payload.clusterId,
+            rateeId: payload.rateeId,
+            reviewId: payload.reviewId,
+            score: payload.score,
+            answersCount: payload.answersCount ?? 1,
+        });
+ 
+        return this.clusterScores.upsert(score);
+    }
+ 
+    async listClusterScores(
+        query: ClusterScoreSearchQuery,
+    ): Promise<ClusterScoreDomain[]> {
+        return this.clusterScores.list(query);
+    }
+ 
+    async getClusterScoreById(
+        id: number,
+    ): Promise<ClusterScoreWithRelationsDomain> {
+        return this.clusterScores.getById(id);
+    }
+ 
+    async getClusterScoreByCycleId(
+        cycleId: number,
+    ): Promise<ClusterScoreWithRelationsDomain[]> {
+        return this.clusterScores.getByCycleId(cycleId);
+    }
+ 
+    async removeClusterScore(id: number): Promise<void> {
+        await this.clusterScores.deleteById(id);
+    }
+ 
+    /**
+     * Centralized method for review stage transitions with validation and history tracking
+     * @param reviewId ID of the review
+     * @param nextStage Target stage
+     * @param actorId User ID who initiated the change (0 for system)
+     * @param actorName Full name of the actor
+     * @param reason Optional reason for the transition
+     */
+    async changeReviewStage(
+        reviewId: number,
+        nextStage: ReviewStage,
+        actorId: number = SYSTEM_ACTOR.ID,
+        actorName: string | null = SYSTEM_ACTOR.NAME,
+        reason?: string,
+    ): Promise<void> {
+        const review = await this.getById(reviewId);
+        const currentStage = review.stage;
+ 
+        // Validate transition is allowed
+        const allowedTransitions = REVIEW_STAGE_TRANSITIONS[currentStage];
+ 
+        if (!allowedTransitions.includes(nextStage)) {
+            throw new BadRequestException(
+                `Invalid review stage transition from ${currentStage} to ${nextStage}`,
+            );
+        }
+ 
+        // Perform update and history logging in a transaction
+        await this.prisma.$transaction(async (tx) => {
+            // Update review stage
+            await tx.review.update({
+                where: { id: reviewId },
+                data: { stage: nextStage },
+            });
+ 
+            // Create history record
+            const history = ReviewStageHistoryDomain.create({
+                reviewId,
+                fromStage: currentStage,
+                toStage: nextStage,
+                changedById: actorId || null,
+                changedByName: actorName,
+                reason,
+            });
+ 
+            await this.stageHistory.create(history);
+        });
+ 
+        // Emit event for listeners to react
+        this.eventEmitter.emit(
+            'review.stage.changed',
+            new ReviewStageChangedEvent(
+                reviewId,
+                currentStage,
+                nextStage,
+                actorId,
+                actorName,
+                reason,
+            ),
+        );
+    }
+ 
+    /**
+     * Get stage change history for a review
+     */
+    async getStageHistory(
+        reviewId: number,
+    ): Promise<ReviewStageHistoryDomain[]> {
+        return this.stageHistory.findByReviewId(reviewId);
+    }
+ 
+    /**
+     * REACTIVE TRIGGER: Checks if all respondents for a review have completed their responses.
+     * If yes, automatically transitions the review to the PREPARING_REPORT stage.
+     * @param reviewId The review identifier.
+     */
+    async completeReview(reviewId: number): Promise<void> {
+        await this.getById(reviewId);
+ 
+        // Get all respondents for this review
+        const respondents = await this.respondents.listByReview(reviewId, {});
+ 
+        // Check if there are any pending or in-progress respondents
+        const incompleteRespondents = respondents.filter(
+            (r) =>
+                r.responseStatus === ResponseStatus.PENDING ||
+                r.responseStatus === ResponseStatus.IN_PROGRESS,
+        );
+        const hasPendingResponses = incompleteRespondents.length > 0;
+ 
+        // If no pending responses, trigger report generation
+        if (!hasPendingResponses && respondents.length > 0) {
+            await this.changeReviewStage(
+                reviewId,
+                ReviewStage.FINISHED,
+                SYSTEM_ACTOR.ID,
+                SYSTEM_ACTOR.NAME,
+                REVIEW_TRANSITION_REASONS.ALL_RESPONSES_COLLECTED,
+            );
+        } else {
+            throw new BadRequestException(
+                'All responses must be collected to finish review #' +
+                    reviewId +
+                    '. Incomplete responses count: ' +
+                    incompleteRespondents.length,
+            );
+        }
+    }
+ 
+    /**
+     * MANUAL TRIGGER: HR or ADMIN force-finishes a review
+     * Transitions review to PREPARING_REPORT regardless of pending responses
+     */
+    async forceFinishReview(
+        reviewId: number,
+        actorId: number,
+        actorName: string,
+    ): Promise<void> {
+        await this.changeReviewStage(
+            reviewId,
+            ReviewStage.FINISHED,
+            actorId,
+            actorName,
+            REVIEW_TRANSITION_REASONS.FORCE_FINISH,
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/answer.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/answer.mapper.ts.html new file mode 100644 index 00000000..7dc8b191 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/answer.mapper.ts.html @@ -0,0 +1,244 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/answer.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers answer.mapper.ts

+
+ +
+ 87.5% + Statements + 7/8 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 75% + Functions + 3/4 +
+ + +
+ 87.5% + Lines + 7/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54  +  +  +  +  +  +19x +19x +19x +  +19x +  +16x +  +  +  +  +  +  +  +  +  +  +  +  +  +11x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  + 
import {
+    Prisma,
+    Answer as PrismaAnswer,
+    RespondentCategory as PrismaRespondentCategory,
+} from '@intra/database';
+import { RespondentCategory } from '@intra/shared-kernel';
+import { AnswerDomain } from '../../domain/answer.domain';
+import { QuestionMapper } from './question.mapper';
+import { RespondentMapper } from './respondent.mapper';
+ 
+export class AnswerMapper {
+    static toDomain(answer: PrismaAnswer): AnswerDomain {
+        return AnswerDomain.create({
+            id: answer.id,
+            reviewId: answer.reviewId,
+            questionId: answer.questionId,
+            respondentCategory: RespondentMapper.fromPrismaCategory(
+                answer.respondentCategory,
+            ),
+            answerType: QuestionMapper.fromPrismaAnswerType(answer.answerType),
+            numericalValue: answer.numericalValue,
+            textValue: answer.textValue,
+        });
+    }
+ 
+    static toPrisma(answer: AnswerDomain): Prisma.AnswerUncheckedCreateInput {
+        return {
+            reviewId: answer.reviewId,
+            questionId: answer.questionId,
+            respondentCategory: RespondentMapper.toPrismaCategory(
+                answer.respondentCategory,
+            ),
+            answerType: QuestionMapper.toPrismaAnswerType(answer.answerType),
+            numericalValue: answer.numericalValue,
+            textValue: answer.textValue,
+            createdAt: answer.createdAt,
+        };
+    }
+ 
+    static toPrismaCategory(
+        domainCategory: RespondentCategory,
+    ): PrismaRespondentCategory {
+        return domainCategory
+            .toString()
+            .toUpperCase() as PrismaRespondentCategory;
+    }
+ 
+    static fromPrismaCategory(
+        prismaCategory: PrismaRespondentCategory,
+    ): RespondentCategory {
+        return prismaCategory.toString().toUpperCase() as RespondentCategory;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cluster-score-analytics.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cluster-score-analytics.mapper.ts.html new file mode 100644 index 00000000..a15f511a --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cluster-score-analytics.mapper.ts.html @@ -0,0 +1,259 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/cluster-score-analytics.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers cluster-score-analytics.mapper.ts

+
+ +
+ 100% + Statements + 6/6 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59  +  +  +  +12x +12x +12x +  +12x +  +  +  +14x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +9x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    Prisma,
+    ClusterScoreAnalytics as PrismaClusterScoreAnalytics,
+} from '@intra/database';
+import Decimal from 'decimal.js';
+import { ClusterScoreAnalyticsDomain } from '../../domain/cluster-score-analytics.domain';
+import { ClusterScoreMapper } from './cluster-score.mapper';
+ 
+export class ClusterScoreAnalyticsMapper {
+    static toDomain(
+        analytics: PrismaClusterScoreAnalytics,
+    ): ClusterScoreAnalyticsDomain {
+        return ClusterScoreAnalyticsDomain.create({
+            id: analytics.id,
+            cycleId: analytics.cycleId,
+            clusterId: analytics.clusterId,
+            employeesCount: new Decimal(analytics.employeesCount),
+            employeeDensity: new Decimal(analytics.employeeDensity),
+            lowerBound: new Decimal(analytics.lowerBound),
+            upperBound: new Decimal(analytics.upperBound),
+            minScore: new Decimal(analytics.minScore),
+            maxScore: new Decimal(analytics.maxScore),
+            averageScore: new Decimal(analytics.averageScore),
+            createdAt: analytics.createdAt,
+            updatedAt: analytics.updatedAt,
+        });
+    }
+ 
+    static toPrisma(
+        analytics: ClusterScoreAnalyticsDomain,
+    ): Prisma.ClusterScoreAnalyticsUncheckedCreateInput {
+        return {
+            cycleId: analytics.cycleId,
+            clusterId: analytics.clusterId,
+            lowerBound: ClusterScoreMapper.toScoreDecimalString(
+                analytics.lowerBound,
+            ),
+            upperBound: ClusterScoreMapper.toScoreDecimalString(
+                analytics.upperBound,
+            ),
+            employeesCount: ClusterScoreMapper.toScoreDecimalString(
+                analytics.employeesCount,
+            ),
+            employeeDensity: ClusterScoreMapper.toScoreDecimalString(
+                analytics.employeeDensity,
+            ),
+            minScore: ClusterScoreMapper.toScoreDecimalString(
+                analytics.minScore,
+            ),
+            maxScore: ClusterScoreMapper.toScoreDecimalString(
+                analytics.maxScore,
+            ),
+            averageScore: ClusterScoreMapper.toScoreDecimalString(
+                analytics.averageScore,
+            ),
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cluster-score.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cluster-score.mapper.ts.html new file mode 100644 index 00000000..6032e279 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cluster-score.mapper.ts.html @@ -0,0 +1,352 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/cluster-score.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers cluster-score.mapper.ts

+
+ +
+ 100% + Statements + 11/11 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 11/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90  +  +  +  +  +  +12x +12x +12x +12x +12x +12x +  +12x +  +10x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +71x +  +  + 
import {
+    Prisma,
+    Cluster as PrismaCluster,
+    ClusterScore as PrismaClusterScore,
+    User as PrismaUser,
+} from '@intra/database';
+import Decimal from 'decimal.js';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { UserMapper } from 'src/contexts/identity/infrastructure/mappers/user.mapper';
+import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain';
+import { ClusterScoreWithRelationsDomain } from '../../domain/cluster-score-with-relations.domain';
+import { ClusterScoreDomain } from '../../domain/cluster-score.domain';
+ 
+export class ClusterScoreMapper {
+    static toDomain(score: PrismaClusterScore): ClusterScoreDomain {
+        return ClusterScoreDomain.create({
+            id: score.id,
+            cycleId: score.cycleId,
+            clusterId: score.clusterId,
+            rateeId: score.rateeId,
+            reviewId: score.reviewId,
+            score: new Decimal(score.score),
+            answersCount: score.answersCount,
+            createdAt: score.createdAt,
+            updatedAt: score.updatedAt,
+        });
+    }
+ 
+    static toPrisma(
+        score: ClusterScoreDomain,
+    ): Prisma.ClusterScoreUncheckedCreateInput {
+        return {
+            cycleId: score.cycleId,
+            clusterId: score.clusterId,
+            rateeId: score.rateeId,
+            reviewId: score.reviewId,
+            score: ClusterScoreMapper.toScoreDecimalString(score.score),
+            answersCount: score.answersCount,
+        };
+    }
+ 
+    static toDomainWithRelations(
+        score: PrismaClusterScore & {
+            cluster: PrismaCluster;
+            ratee: PrismaUser;
+        },
+    ): ClusterScoreWithRelationsDomain {
+        return ClusterScoreWithRelationsDomain.create({
+            id: score.id,
+            cycleId: score.cycleId,
+            clusterId: score.clusterId,
+            rateeId: score.rateeId,
+            reviewId: score.reviewId,
+            score: new Decimal(score.score),
+            answersCount: score.answersCount,
+            createdAt: score.createdAt,
+            updatedAt: score.updatedAt,
+            cluster: ClusterDomain.create({
+                id: score.cluster.id,
+                competenceId: score.cluster.competenceId,
+                lowerBound: new Decimal(score.cluster.lowerBound).toNumber(),
+                upperBound: new Decimal(score.cluster.upperBound).toNumber(),
+                title: score.cluster.title,
+                description: score.cluster.description,
+                createdAt: score.cluster.createdAt,
+                updatedAt: score.cluster.updatedAt,
+            }),
+            ratee: UserDomain.create({
+                id: score.ratee.id,
+                firstName: score.ratee.firstName,
+                secondName: score.ratee.secondName ?? undefined,
+                lastName: score.ratee.lastName,
+                fullName: score.ratee.fullName,
+                email: score.ratee.email,
+                avatarUrl: score.ratee.avatarUrl,
+                status: UserMapper.toDomainStatus(score.ratee.status),
+                positionId: score.ratee.positionId,
+                teamId: score.ratee.teamId,
+                managerId: score.ratee.managerId,
+                createdAt: score.ratee.createdAt,
+                updatedAt: score.ratee.updatedAt,
+            }),
+        });
+    }
+ 
+    static toScoreDecimalString(value: Decimal.Value): string {
+        return new Decimal(value).toDecimalPlaces(4).toFixed(4);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cycle-stage-history.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cycle-stage-history.mapper.ts.html new file mode 100644 index 00000000..9a63c263 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cycle-stage-history.mapper.ts.html @@ -0,0 +1,187 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/cycle-stage-history.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers cycle-stage-history.mapper.ts

+
+ +
+ 100% + Statements + 5/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 5/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35  +  +  +  +12x +12x +  +12x +  +18x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +13x +  +  +  +  +  +  +  +  +  + 
import {
+    Prisma,
+    CycleStageHistory as PrismaCycleStageHistory,
+} from '@intra/database';
+import { CycleStageHistoryDomain } from '../../domain/cycle-stage-history.domain';
+import { CycleMapper } from './cycle.mapper';
+ 
+export class CycleStageHistoryMapper {
+    static toDomain(history: PrismaCycleStageHistory): CycleStageHistoryDomain {
+        return CycleStageHistoryDomain.create({
+            id: history.id,
+            cycleId: history.cycleId,
+            fromStage: CycleMapper.fromPrismaStage(history.fromStage),
+            toStage: CycleMapper.fromPrismaStage(history.toStage),
+            changedById: history.changedById,
+            changedByName: history.changedByName,
+            reason: history.reason,
+            createdAt: history.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        history: CycleStageHistoryDomain,
+    ): Prisma.CycleStageHistoryUncheckedCreateInput {
+        return {
+            cycleId: history.cycleId,
+            fromStage: CycleMapper.toPrismaStage(history.fromStage),
+            toStage: CycleMapper.toPrismaStage(history.toStage),
+            changedById: history.changedById,
+            changedByName: history.changedByName,
+            reason: history.reason,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cycle.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cycle.mapper.ts.html new file mode 100644 index 00000000..2920692a --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/cycle.mapper.ts.html @@ -0,0 +1,241 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/cycle.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers cycle.mapper.ts

+
+ +
+ 100% + Statements + 6/6 +
+ + +
+ 50% + Branches + 1/2 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53  +  +  +  +  +  +20x +  +20x +  +227x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +173x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +201x +  +  +  +263x +  +  + 
import {
+    Prisma,
+    Cycle as PrismaCycle,
+    CycleStage as PrismaCycleStage,
+} from '@intra/database';
+import { CycleStage } from '@intra/shared-kernel';
+import { CycleDomain } from '../../domain/cycle.domain';
+ 
+export class CycleMapper {
+    static toDomain(cycle: PrismaCycle): CycleDomain {
+        return CycleDomain.create({
+            id: cycle.id,
+            title: cycle.title,
+            description: cycle.description,
+            hrId: cycle.hrId,
+            minRespondentsThreshold: cycle.minRespondentsThreshold,
+            stage: CycleMapper.fromPrismaStage(cycle.stage),
+            isActive: cycle.isActive ?? true,
+            startDate: cycle.startDate,
+            reviewDeadline: cycle.reviewDeadline,
+            approvalDeadline: cycle.approvalDeadline,
+            responseDeadline: cycle.responseDeadline,
+            endDate: cycle.endDate,
+            createdAt: cycle.createdAt,
+            updatedAt: cycle.updatedAt,
+        });
+    }
+ 
+    static toPrisma(cycle: CycleDomain): Prisma.CycleUncheckedCreateInput {
+        return {
+            title: cycle.title,
+            description: cycle.description,
+            hrId: cycle.hrId,
+            minRespondentsThreshold: cycle.minRespondentsThreshold,
+            stage: CycleMapper.toPrismaStage(cycle.stage),
+            isActive: cycle.isActive,
+            startDate: cycle.startDate,
+            reviewDeadline: cycle.reviewDeadline,
+            approvalDeadline: cycle.approvalDeadline,
+            responseDeadline: cycle.responseDeadline,
+            endDate: cycle.endDate,
+        };
+    }
+ 
+    static toPrismaStage(domainStage: CycleStage): PrismaCycleStage {
+        return domainStage.toString().toUpperCase() as PrismaCycleStage;
+    }
+ 
+    static fromPrismaStage(prismaStage: PrismaCycleStage): CycleStage {
+        return prismaStage.toString().toUpperCase() as CycleStage;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/index.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/index.html new file mode 100644 index 00000000..03d9d867 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/index.html @@ -0,0 +1,266 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers + + + + + + + + + +
+
+

All files feedback360/infrastructure/mappers

+
+ +
+ 98.57% + Statements + 69/70 +
+ + +
+ 81.25% + Branches + 13/16 +
+ + +
+ 97.22% + Functions + 35/36 +
+ + +
+ 98.57% + Lines + 69/70 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
answer.mapper.ts +
+
87.5%7/8100%0/075%3/487.5%7/8
cluster-score-analytics.mapper.ts +
+
100%6/6100%0/0100%2/2100%6/6
cluster-score.mapper.ts +
+
100%11/11100%2/2100%4/4100%11/11
cycle-stage-history.mapper.ts +
+
100%5/5100%0/0100%2/2100%5/5
cycle.mapper.ts +
+
100%6/650%1/2100%4/4100%6/6
question.mapper.ts +
+
100%6/650%1/2100%4/4100%6/6
respondent.mapper.ts +
+
100%8/8100%0/0100%6/6100%8/8
review-question-relation.mapper.ts +
+
100%5/590%9/10100%2/2100%5/5
review-stage-history.mapper.ts +
+
100%5/5100%0/0100%2/2100%5/5
review.mapper.ts +
+
100%6/6100%0/0100%4/4100%6/6
reviewer.mapper.ts +
+
100%4/4100%0/0100%2/2100%4/4
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/question.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/question.mapper.ts.html new file mode 100644 index 00000000..3eadee8b --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/question.mapper.ts.html @@ -0,0 +1,220 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/question.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers question.mapper.ts

+
+ +
+ 100% + Statements + 6/6 +
+ + +
+ 50% + Branches + 1/2 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46  +  +  +  +  +  +28x +  +28x +  +47x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +36x +  +  +  +  +  +  +  +  +  +  +54x +  +  +  +71x +  +  + 
import {
+    Prisma,
+    AnswerType as PrismaAnswerType,
+    Question as PrismaQuestion,
+} from '@intra/database';
+import { AnswerType } from '@intra/shared-kernel';
+import { QuestionDomain } from '../../domain/question.domain';
+ 
+export class QuestionMapper {
+    static toDomain(question: PrismaQuestion): QuestionDomain {
+        return QuestionDomain.create({
+            id: question.id,
+            cycleId: question.cycleId,
+            questionTemplateId: question.questionTemplateId,
+            title: question.title,
+            answerType: QuestionMapper.fromPrismaAnswerType(
+                question.answerType,
+            ),
+            competenceId: question.competenceId,
+            isForSelfassessment: question.isForSelfassessment ?? false,
+            createdAt: question.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        question: QuestionDomain,
+    ): Prisma.QuestionUncheckedCreateInput {
+        return {
+            cycleId: question.cycleId,
+            questionTemplateId: question.questionTemplateId,
+            title: question.title,
+            answerType: QuestionMapper.toPrismaAnswerType(question.answerType),
+            competenceId: question.competenceId,
+            isForSelfassessment: question.isForSelfassessment,
+        };
+    }
+ 
+    static toPrismaAnswerType(domainType: AnswerType): PrismaAnswerType {
+        return domainType.toString().toUpperCase() as PrismaAnswerType;
+    }
+ 
+    static fromPrismaAnswerType(prismaType: PrismaAnswerType): AnswerType {
+        return prismaType.toString().toUpperCase() as AnswerType;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/respondent.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/respondent.mapper.ts.html new file mode 100644 index 00000000..d948ea53 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/respondent.mapper.ts.html @@ -0,0 +1,334 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/respondent.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers respondent.mapper.ts

+
+ +
+ 100% + Statements + 8/8 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 8/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84  +  +  +  +  +  +  +19x +  +19x +  +16x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +10x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +  +  +  +  +  +23x +  +  +  +  +  +  +  +16x +  +  +  +  +  +32x +  +  + 
import {
+    Prisma,
+    Respondent as PrismaRespondent,
+    RespondentCategory as PrismaRespondentCategory,
+    ResponseStatus as PrismaResponseStatus,
+} from '@intra/database';
+import { RespondentCategory, ResponseStatus } from '@intra/shared-kernel';
+import { RespondentDomain } from '../../domain/respondent.domain';
+ 
+export class RespondentMapper {
+    static toDomain(relation: PrismaRespondent): RespondentDomain {
+        return RespondentDomain.create({
+            id: relation.id,
+            reviewId: relation.reviewId,
+            respondentId: relation.respondentId,
+            fullName: relation.fullName,
+            category: RespondentMapper.fromPrismaCategory(relation.category),
+            responseStatus: RespondentMapper.fromPrismaResponseStatus(
+                relation.responseStatus,
+            ),
+            respondentNote: relation.respondentNote,
+            hrNote: relation.hrNote,
+            positionId: relation.positionId,
+            positionTitle: relation.positionTitle,
+            teamId: relation.teamId,
+            teamTitle: relation.teamTitle,
+            invitedAt: relation.invitedAt,
+            canceledAt: relation.canceledAt,
+            respondedAt: relation.respondedAt,
+            createdAt: relation.createdAt,
+            updatedAt: relation.updatedAt,
+        });
+    }
+ 
+    static toPrisma(
+        respondent: RespondentDomain,
+    ): Prisma.RespondentUncheckedCreateInput {
+        return {
+            reviewId: respondent.reviewId,
+            respondentId: respondent.respondentId,
+            fullName: respondent.fullName,
+            category: RespondentMapper.toPrismaCategory(respondent.category),
+            responseStatus: RespondentMapper.toPrismaResponseStatus(
+                respondent.responseStatus,
+            ),
+            respondentNote: respondent.respondentNote,
+            hrNote: respondent.hrNote,
+            positionId: respondent.positionId,
+            positionTitle: respondent.positionTitle,
+            teamId: respondent.teamId,
+            teamTitle: respondent.teamTitle,
+            invitedAt: respondent.invitedAt,
+            canceledAt: respondent.canceledAt,
+            respondedAt: respondent.respondedAt,
+        };
+    }
+ 
+    static toPrismaResponseStatus(
+        domainStatus: ResponseStatus,
+    ): PrismaResponseStatus {
+        return domainStatus.toString().toUpperCase() as PrismaResponseStatus;
+    }
+ 
+    static toPrismaCategory(
+        domainCategory: RespondentCategory,
+    ): PrismaRespondentCategory {
+        return domainCategory
+            .toString()
+            .toUpperCase() as PrismaRespondentCategory;
+    }
+ 
+    static fromPrismaResponseStatus(
+        prismaStatus: PrismaResponseStatus,
+    ): ResponseStatus {
+        return prismaStatus.toString().toUpperCase() as ResponseStatus;
+    }
+ 
+    static fromPrismaCategory(
+        prismaCategory: PrismaRespondentCategory,
+    ): RespondentCategory {
+        return prismaCategory.toString().toUpperCase() as RespondentCategory;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review-question-relation.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review-question-relation.mapper.ts.html new file mode 100644 index 00000000..7d01b0c4 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review-question-relation.mapper.ts.html @@ -0,0 +1,217 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/review-question-relation.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers review-question-relation.mapper.ts

+
+ +
+ 100% + Statements + 5/5 +
+ + +
+ 90% + Branches + 9/10 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 5/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45  +  +  +  +12x +12x +  +12x +  +  +  +8x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    Prisma,
+    ReviewQuestionRelation as PrismaReviewQuestionRelation,
+} from '@intra/database';
+import { ReviewQuestionRelationDomain } from '../../domain/review-question-relation.domain';
+import { QuestionMapper } from './question.mapper';
+ 
+export class ReviewQuestionRelationMapper {
+    static toDomain(
+        relation: PrismaReviewQuestionRelation,
+    ): ReviewQuestionRelationDomain {
+        return ReviewQuestionRelationDomain.create({
+            id: relation.id,
+            reviewId: relation.reviewId,
+            questionId: relation.questionId,
+            questionTitle: relation.questionTitle,
+            answerType: QuestionMapper.fromPrismaAnswerType(
+                relation.answerType,
+            ),
+            competenceId: relation.competenceId,
+            competenceTitle: relation.competenceTitle,
+            competenceCode: relation.competenceCode ?? null,
+            competenceDescription: relation.competenceDescription ?? null,
+            isForSelfassessment: relation.isForSelfassessment ?? false,
+            createdAt: relation.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        relation: ReviewQuestionRelationDomain,
+    ): Prisma.ReviewQuestionRelationUncheckedCreateInput {
+        return {
+            reviewId: relation.reviewId,
+            questionId: relation.questionId,
+            questionTitle: relation.questionTitle,
+            answerType: QuestionMapper.toPrismaAnswerType(relation.answerType),
+            competenceId: relation.competenceId,
+            competenceTitle: relation.competenceTitle,
+            competenceCode: relation.competenceCode ?? null,
+            competenceDescription: relation.competenceDescription ?? null,
+            isForSelfassessment: relation.isForSelfassessment,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review-stage-history.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review-stage-history.mapper.ts.html new file mode 100644 index 00000000..a6e5d521 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review-stage-history.mapper.ts.html @@ -0,0 +1,193 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/review-stage-history.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers review-stage-history.mapper.ts

+
+ +
+ 100% + Statements + 5/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 5/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37  +  +  +  +12x +12x +  +12x +  +  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  + 
import {
+    Prisma,
+    ReviewStageHistory as PrismaReviewStageHistory,
+} from '@intra/database';
+import { ReviewStageHistoryDomain } from '../../domain/review-stage-history.domain';
+import { ReviewMapper } from './review.mapper';
+ 
+export class ReviewStageHistoryMapper {
+    static toDomain(
+        history: PrismaReviewStageHistory,
+    ): ReviewStageHistoryDomain {
+        return ReviewStageHistoryDomain.create({
+            id: history.id,
+            reviewId: history.reviewId,
+            fromStage: ReviewMapper.fromPrismaStage(history.fromStage),
+            toStage: ReviewMapper.fromPrismaStage(history.toStage),
+            changedById: history.changedById,
+            changedByName: history.changedByName,
+            reason: history.reason,
+            createdAt: history.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        history: ReviewStageHistoryDomain,
+    ): Prisma.ReviewStageHistoryUncheckedCreateInput {
+        return {
+            reviewId: history.reviewId,
+            fromStage: ReviewMapper.toPrismaStage(history.fromStage),
+            toStage: ReviewMapper.toPrismaStage(history.toStage),
+            changedById: history.changedById,
+            changedByName: history.changedByName,
+            reason: history.reason,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review.mapper.ts.html new file mode 100644 index 00000000..6965ccfa --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/review.mapper.ts.html @@ -0,0 +1,271 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/review.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers review.mapper.ts

+
+ +
+ 100% + Statements + 6/6 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63  +  +  +  +  +  +20x +  +20x +  +119x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +100x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +110x +  +  +  +133x +  +  + 
import {
+    Prisma,
+    Review as PrismaReview,
+    ReviewStage as PrismaReviewStage,
+} from '@intra/database';
+import { ReviewStage } from '@intra/shared-kernel';
+import { ReviewDomain } from '../../domain/review.domain';
+ 
+export class ReviewMapper {
+    static toDomain(review: PrismaReview): ReviewDomain {
+        return ReviewDomain.create({
+            id: review.id,
+            rateeId: review.rateeId,
+            rateeFullName: review.rateeFullName,
+            rateePositionId: review.rateePositionId,
+            rateePositionTitle: review.rateePositionTitle,
+            hrId: review.hrId,
+            hrFullName: review.hrFullName,
+            hrNote: review.hrNote,
+            teamId: review.teamId,
+            teamTitle: review.teamTitle,
+            managerId: review.managerId,
+            managerFullName: review.managerFullName,
+            managerPositionId: review.managerPositionId,
+            managerPositionTitle: review.managerPositionTitle,
+            cycleId: review.cycleId,
+            stage: ReviewMapper.fromPrismaStage(review.stage),
+            reportId: review.reportId,
+            createdAt: review.createdAt,
+            updatedAt: review.updatedAt,
+        });
+    }
+ 
+    static toPrisma(review: ReviewDomain): Prisma.ReviewUncheckedCreateInput {
+        return {
+            rateeId: review.rateeId,
+            rateeFullName: review.rateeFullName,
+            rateePositionId: review.rateePositionId,
+            rateePositionTitle: review.rateePositionTitle,
+            hrId: review.hrId,
+            hrFullName: review.hrFullName,
+            hrNote: review.hrNote,
+            teamId: review.teamId,
+            teamTitle: review.teamTitle,
+            managerId: review.managerId,
+            managerFullName: review.managerFullName,
+            managerPositionId: review.managerPositionId,
+            managerPositionTitle: review.managerPositionTitle,
+            cycleId: review.cycleId,
+            stage: ReviewMapper.toPrismaStage(review.stage),
+            reportId: review.reportId,
+        };
+    }
+ 
+    static toPrismaStage(domainStage: ReviewStage): PrismaReviewStage {
+        return domainStage.toString().toUpperCase() as PrismaReviewStage;
+    }
+ 
+    static fromPrismaStage(prismaStage: PrismaReviewStage): ReviewStage {
+        return prismaStage.toString().toUpperCase() as ReviewStage;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/reviewer.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/reviewer.mapper.ts.html new file mode 100644 index 00000000..752274f5 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/mappers/reviewer.mapper.ts.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for feedback360/infrastructure/mappers/reviewer.mapper.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/mappers reviewer.mapper.ts

+
+ +
+ 100% + Statements + 4/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 4/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33  +19x +  +19x +  +10x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x +  +  +  +  +  +  +  +  +  +  + 
import { Prisma, Reviewer as PrismaReviewer } from '@intra/database';
+import { ReviewerDomain } from '../../domain/reviewer.domain';
+ 
+export class ReviewerMapper {
+    static toDomain(relation: PrismaReviewer): ReviewerDomain {
+        return ReviewerDomain.create({
+            id: relation.id,
+            reviewId: relation.reviewId,
+            reviewerId: relation.reviewerId,
+            fullName: relation.fullName,
+            positionId: relation.positionId,
+            positionTitle: relation.positionTitle,
+            teamId: relation.teamId,
+            teamTitle: relation.teamTitle,
+            createdAt: relation.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        reviewer: ReviewerDomain,
+    ): Prisma.ReviewerUncheckedCreateInput {
+        return {
+            reviewId: reviewer.reviewId,
+            reviewerId: reviewer.reviewerId,
+            fullName: reviewer.fullName,
+            positionId: reviewer.positionId,
+            positionTitle: reviewer.positionTitle,
+            teamId: reviewer.teamId,
+            teamTitle: reviewer.teamTitle,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/answer.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/answer.repository.ts.html new file mode 100644 index 00000000..ae0681e5 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/answer.repository.ts.html @@ -0,0 +1,445 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/answer.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories answer.repository.ts

+
+ +
+ 100% + Statements + 34/34 +
+ + +
+ 83.33% + Branches + 20/24 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 100% + Lines + 31/31 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121  +35x +  +  +  +  +  +35x +35x +35x +  +  +  +  +35x +35x +35x +  +  +35x +35x +  +35x +  +  +24x +  +  +  +24x +  +  +  +14x +14x +14x +14x +  +  +  +2x +2x +  +  +  +  +  +  +6x +  +  +  +  +  +6x +  +6x +  +  +4x +  +  +  +  +  +  +4x +5x +5x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +14x +14x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +14x +14x +14x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    AnswerSearchQuery,
+    AnswerSortField,
+    RespondentCategory,
+    SortDirection,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../../application/ports/answer.repository.port';
+import { AnswerDomain } from '../../domain/answer.domain';
+import { AnswerMapper } from '../mappers/answer.mapper';
+import { QuestionMapper } from '../mappers/question.mapper';
+import { RespondentMapper } from '../mappers/respondent.mapper';
+ 
+@Injectable()
+export class AnswerRepository implements AnswerRepositoryPort {
+    readonly [ANSWER_REPOSITORY] = ANSWER_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(answer: AnswerDomain): Promise<AnswerDomain> {
+        const created = await this.prisma.answer.create({
+            data: AnswerMapper.toPrisma(answer),
+        });
+ 
+        return AnswerMapper.toDomain(created);
+    }
+ 
+    async list(query: AnswerSearchQuery): Promise<AnswerDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+        const answers = await this.prisma.answer.findMany({ where, orderBy });
+        return answers.map(AnswerMapper.toDomain);
+    }
+ 
+    async findById(id: number): Promise<AnswerDomain | null> {
+        const answer = await this.prisma.answer.findUnique({ where: { id } });
+        return answer ? AnswerMapper.toDomain(answer) : null;
+    }
+ 
+    async getAnswersCountByRespondentCategories(
+        reviewId: number,
+    ): Promise<{ respondentCategory: RespondentCategory; answers: number }[]> {
+        // 1. Calculate the number of unique questions in the review
+        const questions = await this.prisma.answer.findMany({
+            where: { reviewId },
+            distinct: ['questionId'],
+            select: { questionId: true },
+        });
+ 
+        const questionsCount = questions.length;
+ 
+        if (questionsCount === 0) return [];
+ 
+        // 2. Calculate total number of answers by respondent category
+        const stats = await this.prisma.answer.groupBy({
+            by: ['respondentCategory'],
+            where: { reviewId: reviewId },
+            _count: { _all: true },
+        });
+ 
+        // 3. Map the result: divide the total count by the number of questions
+        return stats.map((stat) => {
+            const totalRows = stat._count._all;
+            return {
+                respondentCategory: AnswerMapper.fromPrismaCategory(
+                    stat.respondentCategory,
+                ),
+                // Math.ceil on the off chance someone skipped a question
+                answers: Math.ceil(totalRows / questionsCount),
+            };
+        });
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.answer.delete({ where: { id } });
+    }
+ 
+    private buildWhere(query: AnswerSearchQuery): Prisma.AnswerWhereInput {
+        const {
+            reviewId,
+            questionId,
+            respondentCategory,
+            answerType,
+            numericalValue,
+            textValue,
+        } = query;
+        return {
+            ...(reviewId ? { reviewId } : {}),
+            ...(questionId ? { questionId } : {}),
+            ...(respondentCategory
+                ? {
+                      respondentCategory:
+                          RespondentMapper.toPrismaCategory(respondentCategory),
+                  }
+                : {}),
+            ...(answerType
+                ? {
+                      answerType: QuestionMapper.toPrismaAnswerType(answerType),
+                  }
+                : {}),
+            ...(numericalValue ? { numericalValue } : {}),
+            ...(textValue
+                ? { textValue: { contains: textValue, mode: 'insensitive' } }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: AnswerSearchQuery,
+    ): Prisma.AnswerOrderByWithRelationInput[] {
+        const field = query.sortBy ?? AnswerSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository.ts.html new file mode 100644 index 00000000..afb2deef --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository.ts.html @@ -0,0 +1,523 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories cluster-score-analytics.repository.ts

+
+ +
+ 100% + Statements + 30/30 +
+ + +
+ 75% + Branches + 18/24 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 100% + Lines + 28/28 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147  +35x +  +  +  +  +  +35x +35x +35x +  +  +  +  +35x +35x +  +  +35x +  +35x +  +35x +  +  +  +  +20x +20x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +20x +  +  +  +8x +  +  +8x +  +  +  +  +  +3x +3x +  +3x +  +  +  +3x +  +  +  +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +7x +  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +3x +3x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    ClusterScoreAnalyticsSearchQuery,
+    ClusterScoreAnalyticsSortField,
+    SortDirection,
+    UpdateClusterScoreAnalyticsPayload,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    CLUSTER_SCORE_ANALYTICS_REPOSITORY,
+    ClusterScoreAnalyticsRepositoryPort,
+} from '../../application/ports/cluster-score-analytics.repository.port';
+import { ClusterScoreAnalyticsDomain } from '../../domain/cluster-score-analytics.domain';
+import { ClusterScoreAnalyticsMapper } from '../mappers/cluster-score-analytics.mapper';
+import { ClusterScoreMapper } from '../mappers/cluster-score.mapper';
+ 
+@Injectable()
+export class ClusterScoreAnalyticsRepository implements ClusterScoreAnalyticsRepositoryPort {
+    readonly [CLUSTER_SCORE_ANALYTICS_REPOSITORY] =
+        CLUSTER_SCORE_ANALYTICS_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async upsert(
+        analytics: ClusterScoreAnalyticsDomain,
+    ): Promise<ClusterScoreAnalyticsDomain> {
+        const prismaAnalytics = ClusterScoreAnalyticsMapper.toPrisma(analytics);
+        const saved = await this.prisma.clusterScoreAnalytics.upsert({
+            where: {
+                cycleId_clusterId: {
+                    cycleId: analytics.cycleId,
+                    clusterId: analytics.clusterId,
+                },
+            },
+            create: prismaAnalytics,
+            update: {
+                employeesCount: prismaAnalytics.employeesCount,
+                employeeDensity: prismaAnalytics.employeeDensity,
+                minScore: prismaAnalytics.minScore,
+                maxScore: prismaAnalytics.maxScore,
+                averageScore: prismaAnalytics.averageScore,
+            },
+        });
+ 
+        return ClusterScoreAnalyticsMapper.toDomain(saved);
+    }
+ 
+    async findById(id: number): Promise<ClusterScoreAnalyticsDomain | null> {
+        const item = await this.prisma.clusterScoreAnalytics.findUnique({
+            where: { id },
+        });
+        return item ? ClusterScoreAnalyticsMapper.toDomain(item) : null;
+    }
+ 
+    async search(
+        query: ClusterScoreAnalyticsSearchQuery,
+    ): Promise<ClusterScoreAnalyticsDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+ 
+        const items = await this.prisma.clusterScoreAnalytics.findMany({
+            where,
+            orderBy,
+        });
+        return items.map(ClusterScoreAnalyticsMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateClusterScoreAnalyticsPayload,
+    ): Promise<ClusterScoreAnalyticsDomain> {
+        const updated = await this.prisma.clusterScoreAnalytics.update({
+            where: { id },
+            data: patch,
+        });
+ 
+        return ClusterScoreAnalyticsMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.clusterScoreAnalytics.delete({ where: { id } });
+    }
+ 
+    async getByCycleId(
+        cycleId: number,
+    ): Promise<ClusterScoreAnalyticsDomain[]> {
+        const items = await this.prisma.clusterScoreAnalytics.findMany({
+            where: { cycleId },
+        });
+        return items.map(ClusterScoreAnalyticsMapper.toDomain);
+    }
+ 
+    private buildWhere(
+        query: ClusterScoreAnalyticsSearchQuery,
+    ): Prisma.ClusterScoreAnalyticsWhereInput {
+        const {
+            cycleId,
+            clusterId,
+            employeesCount,
+            employeeDensity,
+            minScore,
+            maxScore,
+            averageScore,
+        } = query;
+        return {
+            ...(cycleId ? { cycleId } : {}),
+            ...(clusterId ? { clusterId } : {}),
+            ...(employeesCount ? { employeesCount } : {}),
+            ...(employeeDensity !== undefined
+                ? {
+                      employeeDensity:
+                          ClusterScoreMapper.toScoreDecimalString(
+                              employeeDensity,
+                          ),
+                  }
+                : {}),
+            ...(minScore !== undefined
+                ? {
+                      minScore:
+                          ClusterScoreMapper.toScoreDecimalString(minScore),
+                  }
+                : {}),
+            ...(maxScore !== undefined
+                ? {
+                      maxScore:
+                          ClusterScoreMapper.toScoreDecimalString(maxScore),
+                  }
+                : {}),
+            ...(averageScore !== undefined
+                ? {
+                      averageScore:
+                          ClusterScoreMapper.toScoreDecimalString(averageScore),
+                  }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: ClusterScoreAnalyticsSearchQuery,
+    ): Prisma.ClusterScoreAnalyticsOrderByWithRelationInput[] {
+        const field = query.sortBy ?? ClusterScoreAnalyticsSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cluster-score.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cluster-score.repository.ts.html new file mode 100644 index 00000000..01c3995b --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cluster-score.repository.ts.html @@ -0,0 +1,409 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/cluster-score.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories cluster-score.repository.ts

+
+ +
+ 100% + Statements + 30/30 +
+ + +
+ 77.27% + Branches + 17/22 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 100% + Lines + 26/26 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109  +35x +  +  +  +  +35x +35x +35x +  +  +  +  +  +35x +  +  +35x +35x +  +35x +  +  +12x +12x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +  +  +  +9x +9x +9x +  +  +  +9x +  +  +  +2x +  +  +  +  +  +  +2x +1x +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +9x +9x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +9x +9x +9x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    ClusterScoreSearchQuery,
+    ClusterScoreSortField,
+    SortDirection,
+} from '@intra/shared-kernel';
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    CLUSTER_SCORE_REPOSITORY,
+    ClusterScoreRepositoryPort,
+} from '../../application/ports/cluster-score.repository.port';
+import { ClusterScoreWithRelationsDomain } from '../../domain/cluster-score-with-relations.domain';
+import { ClusterScoreDomain } from '../../domain/cluster-score.domain';
+import { ClusterScoreMapper } from '../mappers/cluster-score.mapper';
+ 
+@Injectable()
+export class ClusterScoreRepository implements ClusterScoreRepositoryPort {
+    readonly [CLUSTER_SCORE_REPOSITORY] = CLUSTER_SCORE_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async upsert(score: ClusterScoreDomain): Promise<ClusterScoreDomain> {
+        const prismaScore = ClusterScoreMapper.toPrisma(score);
+        const saved = await this.prisma.clusterScore.upsert({
+            where: {
+                clusterId_rateeId: {
+                    clusterId: score.clusterId,
+                    rateeId: score.rateeId,
+                },
+            },
+            create: prismaScore,
+            update: {
+                cycleId: prismaScore.cycleId,
+                reviewId: prismaScore.reviewId,
+                score: prismaScore.score,
+                answersCount: prismaScore.answersCount,
+            },
+        });
+ 
+        return ClusterScoreMapper.toDomain(saved);
+    }
+ 
+    async list(query: ClusterScoreSearchQuery): Promise<ClusterScoreDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+        const scores = await this.prisma.clusterScore.findMany({
+            where,
+            orderBy,
+        });
+        return scores.map(ClusterScoreMapper.toDomain);
+    }
+ 
+    async getById(id: number): Promise<ClusterScoreWithRelationsDomain> {
+        const score = await this.prisma.clusterScore.findUnique({
+            where: { id },
+            include: {
+                cluster: true,
+                ratee: true,
+            },
+        });
+        if (!score) throw new NotFoundException('Cluster score not found');
+        return ClusterScoreMapper.toDomainWithRelations(score);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.clusterScore.delete({ where: { id } });
+    }
+ 
+    async getByCycleId(
+        cycleId: number,
+    ): Promise<ClusterScoreWithRelationsDomain[]> {
+        const scores = await this.prisma.clusterScore.findMany({
+            where: { cycleId },
+            orderBy: [{ clusterId: 'asc' }, { score: 'asc' }],
+            include: {
+                cluster: true,
+                ratee: true,
+            },
+        });
+        return scores.map((s) => ClusterScoreMapper.toDomainWithRelations(s));
+    }
+ 
+    private buildWhere(
+        query: ClusterScoreSearchQuery,
+    ): Prisma.ClusterScoreWhereInput {
+        const { cycleId, clusterId, rateeId, reviewId, score, answerCount } =
+            query;
+        return {
+            ...(cycleId ? { cycleId } : {}),
+            ...(clusterId ? { clusterId } : {}),
+            ...(rateeId ? { rateeId } : {}),
+            ...(reviewId ? { reviewId } : {}),
+            ...(score !== undefined
+                ? { score: ClusterScoreMapper.toScoreDecimalString(score) }
+                : {}),
+            ...(answerCount ? { answerCount } : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: ClusterScoreSearchQuery,
+    ): Prisma.ClusterScoreOrderByWithRelationInput[] {
+        const field = query.sortBy ?? ClusterScoreSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository.ts.html new file mode 100644 index 00000000..74ad7333 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository.ts.html @@ -0,0 +1,202 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories cycle-stage-history.repository.ts

+
+ +
+ 100% + Statements + 15/15 +
+ + +
+ 83.33% + Branches + 5/6 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 13/13 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +4038x +38x +38x +  +  +  +  +38x +  +  +38x +37x +  +37x +  +  +  +  +17x +  +  +17x +  +  +  +4x +  +  +  +4x +  +  +  +2x +  +  +2x +  +  + 
import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    CYCLE_STAGE_HISTORY_REPOSITORY,
+    CycleStageHistoryRepositoryPort,
+} from '../../application/ports/cycle-stage-history.repository.port';
+import { CycleStageHistoryDomain } from '../../domain/cycle-stage-history.domain';
+import { CycleStageHistoryMapper } from '../mappers/cycle-stage-history.mapper';
+ 
+@Injectable()
+export class CycleStageHistoryRepository implements CycleStageHistoryRepositoryPort {
+    readonly [CYCLE_STAGE_HISTORY_REPOSITORY] = CYCLE_STAGE_HISTORY_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(
+        history: CycleStageHistoryDomain,
+    ): Promise<CycleStageHistoryDomain> {
+        const created = await this.prisma.cycleStageHistory.create({
+            data: CycleStageHistoryMapper.toPrisma(history),
+        });
+        return CycleStageHistoryMapper.toDomain(created);
+    }
+ 
+    async findByCycleId(cycleId: number): Promise<CycleStageHistoryDomain[]> {
+        const items = await this.prisma.cycleStageHistory.findMany({
+            where: { cycleId },
+            orderBy: { createdAt: 'asc' },
+        });
+        return items.map(CycleStageHistoryMapper.toDomain);
+    }
+ 
+    async findById(id: number): Promise<CycleStageHistoryDomain | null> {
+        const item = await this.prisma.cycleStageHistory.findUnique({
+            where: { id },
+        });
+        return item ? CycleStageHistoryMapper.toDomain(item) : null;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cycle.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cycle.repository.ts.html new file mode 100644 index 00000000..b876a032 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/cycle.repository.ts.html @@ -0,0 +1,472 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/cycle.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories cycle.repository.ts

+
+ +
+ 100% + Statements + 26/26 +
+ + +
+ 72.22% + Branches + 26/36 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 24/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130  +38x +  +  +  +  +  +38x +38x +38x +  +  +  +  +38x +  +  +38x +38x +  +38x +  +  +308x +  +  +  +308x +  +  +  +163x +163x +  +  +  +12x +12x +  +12x +  +  +  +  +12x +  +  +  +  +  +  +7x +  +  +  +  +  +  +  +  +  +  +7x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +12x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +12x +12x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    CycleSearchQuery,
+    CycleSortField,
+    SortDirection,
+    UpdateCyclePayload,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    CYCLE_REPOSITORY,
+    CycleRepositoryPort,
+} from '../../application/ports/cycle.repository.port';
+import { CycleDomain } from '../../domain/cycle.domain';
+import { CycleMapper } from '../mappers/cycle.mapper';
+ 
+@Injectable()
+export class CycleRepository implements CycleRepositoryPort {
+    readonly [CYCLE_REPOSITORY] = CYCLE_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(cycle: CycleDomain): Promise<CycleDomain> {
+        const created = await this.prisma.cycle.create({
+            data: CycleMapper.toPrisma(cycle),
+        });
+ 
+        return CycleMapper.toDomain(created);
+    }
+ 
+    async findById(id: number): Promise<CycleDomain | null> {
+        const cycle = await this.prisma.cycle.findUnique({ where: { id } });
+        return cycle ? CycleMapper.toDomain(cycle) : null;
+    }
+ 
+    async search(query: CycleSearchQuery): Promise<CycleDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+ 
+        const items = await this.prisma.cycle.findMany({
+            where,
+            orderBy,
+        });
+ 
+        return items.map(CycleMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateCyclePayload,
+    ): Promise<CycleDomain> {
+        const updated = await this.prisma.cycle.update({
+            where: { id },
+            data: {
+                ...patch,
+                ...(patch.stage
+                    ? {
+                          stage: CycleMapper.toPrismaStage(patch.stage),
+                      }
+                    : {}),
+            },
+        });
+        return CycleMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.cycle.delete({ where: { id } });
+    }
+ 
+    private buildWhere(query: CycleSearchQuery): Prisma.CycleWhereInput {
+        const {
+            title,
+            description,
+            hrId,
+            stage,
+            isActive,
+            search,
+            minRespondentsThreshold,
+            startDate,
+            reviewDeadline,
+            approvalDeadline,
+            responseDeadline,
+            endDate,
+        } = query;
+        return {
+            ...(title
+                ? { title: { contains: title, mode: 'insensitive' } }
+                : {}),
+            ...(description
+                ? {
+                      description: {
+                          contains: description,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(search
+                ? {
+                      OR: [
+                          { title: { contains: search, mode: 'insensitive' } },
+                          {
+                              description: {
+                                  contains: search,
+                                  mode: 'insensitive',
+                              },
+                          },
+                      ],
+                  }
+                : {}),
+            ...(hrId ? { hrId } : {}),
+            ...(minRespondentsThreshold ? { minRespondentsThreshold } : {}),
+            ...(stage ? { stage: CycleMapper.toPrismaStage(stage) } : {}),
+            ...(isActive !== undefined ? { isActive } : {}),
+            ...(startDate ? { startDate } : {}),
+            ...(reviewDeadline ? { reviewDeadline } : {}),
+            ...(approvalDeadline ? { approvalDeadline } : {}),
+            ...(responseDeadline ? { responseDeadline } : {}),
+            ...(endDate ? { endDate } : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: CycleSearchQuery,
+    ): Prisma.CycleOrderByWithRelationInput[] {
+        const field = query.sortBy ?? CycleSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/index.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/index.html new file mode 100644 index 00000000..73c8a2d5 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/index.html @@ -0,0 +1,266 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories + + + + + + + + + +
+
+

All files feedback360/infrastructure/prisma-repositories

+
+ +
+ 100% + Statements + 279/279 +
+ + +
+ 73.02% + Branches + 203/278 +
+ + +
+ 100% + Functions + 79/79 +
+ + +
+ 100% + Lines + 254/254 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
answer.repository.ts +
+
100%34/3483.33%20/24100%9/9100%31/31
cluster-score-analytics.repository.ts +
+
100%30/3075%18/24100%9/9100%28/28
cluster-score.repository.ts +
+
100%30/3077.27%17/22100%9/9100%26/26
cycle-stage-history.repository.ts +
+
100%15/1583.33%5/6100%4/4100%13/13
cycle.repository.ts +
+
100%26/2672.22%26/36100%8/8100%24/24
question.repository.ts +
+
100%24/2487.5%21/24100%7/7100%22/22
respondent.repository.ts +
+
100%29/2966.66%28/42100%8/8100%27/27
review-question-relation.repository.ts +
+
100%25/2560.71%17/28100%6/6100%23/23
review-stage-history.repository.ts +
+
100%15/1583.33%5/6100%4/4100%13/13
review.repository.ts +
+
100%28/2868.18%30/44100%9/9100%26/26
reviewer.repository.ts +
+
100%23/2372.72%16/22100%6/6100%21/21
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/question.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/question.repository.ts.html new file mode 100644 index 00000000..2563fc78 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/question.repository.ts.html @@ -0,0 +1,352 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/question.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories question.repository.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 87.5% + Branches + 21/24 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 22/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90  +44x +  +  +  +  +44x +44x +44x +  +  +  +  +44x +  +  +44x +44x +  +44x +  +  +66x +  +  +  +66x +  +  +  +6x +  +  +6x +  +  +  +10x +10x +10x +10x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +10x +10x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +10x +10x +10x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    QuestionSearchQuery,
+    QuestionSortField,
+    SortDirection,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    QUESTION_REPOSITORY,
+    QuestionRepositoryPort,
+} from '../../application/ports/question.repository.port';
+import { QuestionDomain } from '../../domain/question.domain';
+import { QuestionMapper } from '../mappers/question.mapper';
+ 
+@Injectable()
+export class QuestionRepository implements QuestionRepositoryPort {
+    readonly [QUESTION_REPOSITORY] = QUESTION_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(question: QuestionDomain): Promise<QuestionDomain> {
+        const created = await this.prisma.question.create({
+            data: QuestionMapper.toPrisma(question),
+        });
+ 
+        return QuestionMapper.toDomain(created);
+    }
+ 
+    async findById(id: number): Promise<QuestionDomain | null> {
+        const question = await this.prisma.question.findUnique({
+            where: { id },
+        });
+        return question ? QuestionMapper.toDomain(question) : null;
+    }
+ 
+    async search(query: QuestionSearchQuery): Promise<QuestionDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+        const items = await this.prisma.question.findMany({ where, orderBy });
+        return items.map(QuestionMapper.toDomain);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.question.delete({ where: { id } });
+    }
+ 
+    private buildWhere(query: QuestionSearchQuery): Prisma.QuestionWhereInput {
+        const {
+            cycleId,
+            questionTemplateId,
+            competenceId,
+            answerType,
+            isForSelfassessment,
+            title,
+        } = query;
+        return {
+            ...(cycleId ? { cycleId } : {}),
+            ...(questionTemplateId ? { questionTemplateId } : {}),
+            ...(title
+                ? { title: { contains: title, mode: 'insensitive' } }
+                : {}),
+            ...(answerType
+                ? {
+                      answerType: QuestionMapper.toPrismaAnswerType(answerType),
+                  }
+                : {}),
+            ...(competenceId ? { competenceId } : {}),
+            ...(isForSelfassessment !== undefined
+                ? isForSelfassessment === true
+                    ? { isForSelfassessment: true }
+                    : {
+                          OR: [
+                              { isForSelfassessment: false },
+                              { isForSelfassessment: null },
+                          ],
+                      }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: QuestionSearchQuery,
+    ): Prisma.QuestionOrderByWithRelationInput[] {
+        const field = query.sortBy ?? QuestionSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/respondent.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/respondent.repository.ts.html new file mode 100644 index 00000000..201ecb93 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/respondent.repository.ts.html @@ -0,0 +1,580 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/respondent.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories respondent.repository.ts

+
+ +
+ 100% + Statements + 29/29 +
+ + +
+ 66.66% + Branches + 28/42 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 27/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166  +38x +  +  +  +  +  +38x +38x +38x +  +  +  +  +38x +  +  +38x +37x +  +37x +  +  +26x +  +  +  +26x +  +  +  +9x +  +  +  +9x +1x +  +  +8x +  +  +  +  +  +  +18x +18x +18x +18x +  +  +  +  +18x +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +18x +18x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +18x +18x +18x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    RespondentSearchQuery,
+    RespondentSortField,
+    SortDirection,
+    UpdateRespondentPayload,
+} from '@intra/shared-kernel';
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../../application/ports/respondent.repository.port';
+import { RespondentDomain } from '../../domain/respondent.domain';
+import { RespondentMapper } from '../mappers/respondent.mapper';
+ 
+@Injectable()
+export class RespondentRepository implements RespondentRepositoryPort {
+    readonly [RESPONDENT_REPOSITORY] = RESPONDENT_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(respondent: RespondentDomain): Promise<RespondentDomain> {
+        const created = await this.prisma.respondent.create({
+            data: RespondentMapper.toPrisma(respondent),
+        });
+ 
+        return RespondentMapper.toDomain(created);
+    }
+ 
+    async getById(id: number): Promise<RespondentDomain> {
+        const respondent = await this.prisma.respondent.findUnique({
+            where: { id },
+        });
+ 
+        if (!respondent) {
+            throw new NotFoundException(`Respondent with id ${id} not found`);
+        }
+ 
+        return RespondentMapper.toDomain(respondent);
+    }
+ 
+    async listByReview(
+        reviewId: number,
+        query: RespondentSearchQuery,
+    ): Promise<RespondentDomain[]> {
+        const where = this.buildWhere(query);
+        where.reviewId = reviewId;
+        const orderBy = this.buildOrder(query);
+        const relations = await this.prisma.respondent.findMany({
+            where,
+            orderBy,
+        });
+ 
+        return relations.map(RespondentMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateRespondentPayload,
+    ): Promise<RespondentDomain> {
+        const updated = await this.prisma.respondent.update({
+            where: { id },
+            data: {
+                ...patch,
+                ...(patch.category
+                    ? {
+                          category: RespondentMapper.toPrismaCategory(
+                              patch.category,
+                          ),
+                      }
+                    : {}),
+                ...(patch.responseStatus
+                    ? {
+                          responseStatus:
+                              RespondentMapper.toPrismaResponseStatus(
+                                  patch.responseStatus,
+                              ),
+                      }
+                    : {}),
+            },
+        });
+        return RespondentMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.respondent.delete({ where: { id } });
+    }
+ 
+    private buildWhere(
+        query: RespondentSearchQuery,
+    ): Prisma.RespondentWhereInput {
+        const {
+            reviewId,
+            respondentId,
+            fullName,
+            category,
+            responseStatus,
+            respondentNote,
+            hrNote,
+            positionId,
+            positionTitle,
+            teamId,
+            teamTitle,
+            invitedAt,
+            canceledAt,
+            respondedAt,
+        } = query;
+        return {
+            ...(reviewId ? { reviewId } : {}),
+            ...(respondentId ? { respondentId } : {}),
+            ...(fullName
+                ? { fullName: { contains: fullName, mode: 'insensitive' } }
+                : {}),
+            ...(category
+                ? {
+                      category: RespondentMapper.toPrismaCategory(category),
+                  }
+                : {}),
+            ...(responseStatus
+                ? {
+                      responseStatus:
+                          RespondentMapper.toPrismaResponseStatus(
+                              responseStatus,
+                          ),
+                  }
+                : {}),
+            ...(respondentNote
+                ? {
+                      respondentNote: {
+                          contains: respondentNote,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(hrNote
+                ? { hrNote: { contains: hrNote, mode: 'insensitive' } }
+                : {}),
+            ...(positionId ? { positionId } : {}),
+            ...(positionTitle
+                ? {
+                      positionTitle: {
+                          contains: positionTitle,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(teamId ? { teamId } : {}),
+            ...(teamTitle
+                ? { teamTitle: { contains: teamTitle, mode: 'insensitive' } }
+                : {}),
+            ...(invitedAt ? { invitedAt } : {}),
+            ...(canceledAt ? { canceledAt } : {}),
+            ...(respondedAt ? { respondedAt } : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: RespondentSearchQuery,
+    ): Prisma.RespondentOrderByWithRelationInput[] {
+        const field = query.sortBy ?? RespondentSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review-question-relation.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review-question-relation.repository.ts.html new file mode 100644 index 00000000..2b7e381a --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review-question-relation.repository.ts.html @@ -0,0 +1,535 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/review-question-relation.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories review-question-relation.repository.ts

+
+ +
+ 100% + Statements + 25/25 +
+ + +
+ 60.71% + Branches + 17/28 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 23/23 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151  +35x +  +  +  +  +35x +35x +35x +  +  +  +  +35x +35x +  +  +35x +  +35x +  +35x +  +  +  +  +15x +15x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +15x +  +  +  +  +  +  +11x +11x +11x +11x +  +  +  +11x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +11x +11x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +11x +11x +11x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    ReviewQuestionRelationSearchQuery,
+    ReviewQuestionRelationSortField,
+    SortDirection,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    REVIEW_QUESTION_RELATION_REPOSITORY,
+    ReviewQuestionRelationRepositoryPort,
+} from '../../application/ports/review-question-relation.repository.port';
+import { ReviewQuestionRelationDomain } from '../../domain/review-question-relation.domain';
+import { QuestionMapper } from '../mappers/question.mapper';
+import { ReviewQuestionRelationMapper } from '../mappers/review-question-relation.mapper';
+ 
+@Injectable()
+export class ReviewQuestionRelationRepository implements ReviewQuestionRelationRepositoryPort {
+    readonly [REVIEW_QUESTION_RELATION_REPOSITORY] =
+        REVIEW_QUESTION_RELATION_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async link(
+        relation: ReviewQuestionRelationDomain,
+    ): Promise<ReviewQuestionRelationDomain> {
+        const prismaRelation = ReviewQuestionRelationMapper.toPrisma(relation);
+        const created = await this.prisma.reviewQuestionRelation.upsert({
+            where: {
+                reviewId_questionId: {
+                    reviewId: relation.reviewId,
+                    questionId: relation.questionId,
+                },
+            },
+            create: prismaRelation,
+            update: {
+                questionTitle: prismaRelation.questionTitle,
+                answerType: prismaRelation.answerType,
+                competenceId: prismaRelation.competenceId,
+                competenceTitle: prismaRelation.competenceTitle,
+                competenceCode: prismaRelation.competenceCode,
+                competenceDescription: prismaRelation.competenceDescription,
+                isForSelfassessment: prismaRelation.isForSelfassessment,
+            },
+        });
+ 
+        return ReviewQuestionRelationMapper.toDomain(created);
+    }
+ 
+    async listByReview(
+        reviewId: number,
+        query: ReviewQuestionRelationSearchQuery,
+    ): Promise<ReviewQuestionRelationDomain[]> {
+        const where = this.buildWhere(query);
+        where.reviewId = reviewId;
+        const orderBy = this.buildOrder(query);
+        const relations = await this.prisma.reviewQuestionRelation.findMany({
+            where,
+            orderBy,
+        });
+        return relations.map(ReviewQuestionRelationMapper.toDomain);
+    }
+ 
+    async unlink(reviewId: number, questionId: number): Promise<void> {
+        await this.prisma.reviewQuestionRelation.delete({
+            where: {
+                reviewId_questionId: {
+                    reviewId,
+                    questionId,
+                },
+            },
+        });
+    }
+ 
+    private buildWhere(
+        query: ReviewQuestionRelationSearchQuery,
+    ): Prisma.ReviewQuestionRelationWhereInput {
+        const {
+            reviewId,
+            questionId,
+            questionTitle,
+            answerType,
+            competenceId,
+            competenceTitle,
+            competenceCode,
+            competenceDescription,
+            isForSelfassessment,
+        } = query;
+        return {
+            ...(reviewId ? { reviewId } : {}),
+            ...(questionId ? { questionId } : {}),
+            ...(questionTitle
+                ? {
+                      questionTitle: {
+                          contains: questionTitle,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(answerType
+                ? {
+                      answerType: QuestionMapper.toPrismaAnswerType(answerType),
+                  }
+                : {}),
+            ...(competenceId ? { competenceId } : {}),
+            ...(competenceTitle
+                ? {
+                      competenceTitle: {
+                          contains: competenceTitle,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(competenceCode
+                ? {
+                      competenceCode: {
+                          contains: competenceCode,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(competenceDescription
+                ? {
+                      competenceDescription: {
+                          contains: competenceDescription,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(isForSelfassessment !== undefined
+                ? isForSelfassessment === true
+                    ? { isForSelfassessment: true }
+                    : {
+                          OR: [
+                              { isForSelfassessment: false },
+                              { isForSelfassessment: null },
+                          ],
+                      }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: ReviewQuestionRelationSearchQuery,
+    ): Prisma.ReviewQuestionRelationOrderByWithRelationInput[] {
+        const field = query.sortBy ?? ReviewQuestionRelationSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review-stage-history.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review-stage-history.repository.ts.html new file mode 100644 index 00000000..1fa06396 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review-stage-history.repository.ts.html @@ -0,0 +1,211 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/review-stage-history.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories review-stage-history.repository.ts

+
+ +
+ 100% + Statements + 15/15 +
+ + +
+ 83.33% + Branches + 5/6 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 13/13 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +4335x +35x +35x +  +  +  +  +35x +  +  +35x +  +35x +  +35x +  +  +  +  +13x +  +  +13x +  +  +  +  +  +4x +  +  +  +4x +  +  +  +2x +  +  +2x +  +  + 
import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    REVIEW_STAGE_HISTORY_REPOSITORY,
+    ReviewStageHistoryRepositoryPort,
+} from '../../application/ports/review-stage-history.repository.port';
+import { ReviewStageHistoryDomain } from '../../domain/review-stage-history.domain';
+import { ReviewStageHistoryMapper } from '../mappers/review-stage-history.mapper';
+ 
+@Injectable()
+export class ReviewStageHistoryRepository implements ReviewStageHistoryRepositoryPort {
+    readonly [REVIEW_STAGE_HISTORY_REPOSITORY] =
+        REVIEW_STAGE_HISTORY_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(
+        history: ReviewStageHistoryDomain,
+    ): Promise<ReviewStageHistoryDomain> {
+        const created = await this.prisma.reviewStageHistory.create({
+            data: ReviewStageHistoryMapper.toPrisma(history),
+        });
+        return ReviewStageHistoryMapper.toDomain(created);
+    }
+ 
+    async findByReviewId(
+        reviewId: number,
+    ): Promise<ReviewStageHistoryDomain[]> {
+        const items = await this.prisma.reviewStageHistory.findMany({
+            where: { reviewId },
+            orderBy: { createdAt: 'asc' },
+        });
+        return items.map(ReviewStageHistoryMapper.toDomain);
+    }
+ 
+    async findById(id: number): Promise<ReviewStageHistoryDomain | null> {
+        const item = await this.prisma.reviewStageHistory.findUnique({
+            where: { id },
+        });
+        return item ? ReviewStageHistoryMapper.toDomain(item) : null;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review.repository.ts.html new file mode 100644 index 00000000..521a9793 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/review.repository.ts.html @@ -0,0 +1,580 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/review.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories review.repository.ts

+
+ +
+ 100% + Statements + 28/28 +
+ + +
+ 68.18% + Branches + 30/44 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 100% + Lines + 26/26 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166  +38x +  +  +  +  +  +38x +38x +38x +  +  +  +  +38x +  +  +38x +38x +  +38x +  +  +222x +  +  +  +222x +  +  +  +103x +103x +  +  +  +10x +10x +10x +10x +  +  +  +23x +  +  +22x +  +  +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +10x +10x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +10x +10x +10x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    ReviewSearchQuery,
+    ReviewSortField,
+    SortDirection,
+    UpdateReviewPayload,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../../application/ports/review.repository.port';
+import { ReviewDomain } from '../../domain/review.domain';
+import { ReviewMapper } from '../mappers/review.mapper';
+ 
+@Injectable()
+export class ReviewRepository implements ReviewRepositoryPort {
+    readonly [REVIEW_REPOSITORY] = REVIEW_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(review: ReviewDomain): Promise<ReviewDomain> {
+        const created = await this.prisma.review.create({
+            data: ReviewMapper.toPrisma(review),
+        });
+ 
+        return ReviewMapper.toDomain(created);
+    }
+ 
+    async findById(id: number): Promise<ReviewDomain | null> {
+        const review = await this.prisma.review.findUnique({ where: { id } });
+        return review ? ReviewMapper.toDomain(review) : null;
+    }
+ 
+    async search(query: ReviewSearchQuery): Promise<ReviewDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+        const items = await this.prisma.review.findMany({ where, orderBy });
+        return items.map(ReviewMapper.toDomain);
+    }
+ 
+    async listByCycleId(cycleId: number): Promise<ReviewDomain[]> {
+        const reviews = await this.prisma.review.findMany({
+            where: { cycleId },
+        });
+        return reviews.map(ReviewMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateReviewPayload,
+    ): Promise<ReviewDomain> {
+        const updated = await this.prisma.review.update({
+            where: { id },
+            data: {
+                ...patch,
+                ...(patch.stage
+                    ? {
+                          stage: ReviewMapper.toPrismaStage(patch.stage),
+                      }
+                    : {}),
+            },
+        });
+        return ReviewMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        // Cascade-clean dependent rows that don't have ON DELETE CASCADE
+        // configured at the schema level. This is safe because the service
+        // layer guarantees there are no submitted answers at this point.
+        await this.prisma.$transaction([
+            this.prisma.reviewQuestionRelation.deleteMany({
+                where: { reviewId: id },
+            }),
+            this.prisma.respondent.deleteMany({ where: { reviewId: id } }),
+            this.prisma.reviewer.deleteMany({ where: { reviewId: id } }),
+            this.prisma.reviewStageHistory.deleteMany({
+                where: { reviewId: id },
+            }),
+            this.prisma.review.delete({ where: { id } }),
+        ]);
+    }
+ 
+    private buildWhere(query: ReviewSearchQuery): Prisma.ReviewWhereInput {
+        const {
+            cycleId,
+            rateeId,
+            rateeFullName,
+            hrId,
+            hrFullName,
+            rateePositionId,
+            teamId,
+            managerId,
+            managerFullName,
+            managerPositionId,
+            managerPositionTitle,
+            stage,
+            rateePositionTitle,
+            hrNote,
+            teamTitle,
+            reportId,
+        } = query;
+        return {
+            ...(rateeId ? { rateeId } : {}),
+            ...(rateeFullName
+                ? {
+                      rateeFullName: {
+                          contains: rateeFullName,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(rateePositionId ? { rateePositionId } : {}),
+            ...(rateePositionTitle
+                ? {
+                      rateePositionTitle: {
+                          contains: rateePositionTitle,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(hrId ? { hrId } : {}),
+            ...(hrFullName
+                ? { hrFullName: { contains: hrFullName, mode: 'insensitive' } }
+                : {}),
+            ...(hrNote
+                ? { hrNote: { contains: hrNote, mode: 'insensitive' } }
+                : {}),
+            ...(teamId ? { teamId } : {}),
+            ...(teamTitle
+                ? { teamTitle: { contains: teamTitle, mode: 'insensitive' } }
+                : {}),
+            ...(managerId ? { managerId } : {}),
+            ...(managerFullName
+                ? {
+                      managerFullName: {
+                          contains: managerFullName,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(managerPositionId ? { managerPositionId } : {}),
+            ...(managerPositionTitle
+                ? {
+                      managerPositionTitle: {
+                          contains: managerPositionTitle,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(cycleId ? { cycleId } : {}),
+            ...(stage ? { stage: ReviewMapper.toPrismaStage(stage) } : {}),
+            ...(reportId ? { reportId } : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: ReviewSearchQuery,
+    ): Prisma.ReviewOrderByWithRelationInput[] {
+        const field = query.sortBy ?? ReviewSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/reviewer.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/reviewer.repository.ts.html new file mode 100644 index 00000000..53950e88 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/feedback360/infrastructure/prisma-repositories/reviewer.repository.ts.html @@ -0,0 +1,334 @@ + + + + + + Code coverage report for feedback360/infrastructure/prisma-repositories/reviewer.repository.ts + + + + + + + + + +
+
+

All files / feedback360/infrastructure/prisma-repositories reviewer.repository.ts

+
+ +
+ 100% + Statements + 23/23 +
+ + +
+ 72.72% + Branches + 16/22 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 21/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84  +38x +  +  +  +  +38x +38x +38x +  +  +  +  +38x +  +  +38x +37x +  +37x +  +  +9x +  +  +9x +  +  +  +  +  +  +11x +11x +11x +11x +11x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +11x +11x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +11x +11x +11x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    ReviewerSearchQuery,
+    ReviewerSortField,
+    SortDirection,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from '../../application/ports/reviewer.repository.port';
+import { ReviewerDomain } from '../../domain/reviewer.domain';
+import { ReviewerMapper } from '../mappers/reviewer.mapper';
+ 
+@Injectable()
+export class ReviewerRepository implements ReviewerRepositoryPort {
+    readonly [REVIEWER_REPOSITORY] = REVIEWER_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(reviewer: ReviewerDomain): Promise<ReviewerDomain> {
+        const created = await this.prisma.reviewer.create({
+            data: ReviewerMapper.toPrisma(reviewer),
+        });
+        return ReviewerMapper.toDomain(created);
+    }
+ 
+    async listByReview(
+        reviewId: number,
+        query: ReviewerSearchQuery,
+    ): Promise<ReviewerDomain[]> {
+        const where = this.buildWhere(query);
+        where.reviewId = reviewId;
+        const orderBy = this.buildOrder(query);
+        const items = await this.prisma.reviewer.findMany({ where, orderBy });
+        return items.map(ReviewerMapper.toDomain);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.reviewer.delete({ where: { id } });
+    }
+ 
+    private buildWhere(query: ReviewerSearchQuery): Prisma.ReviewerWhereInput {
+        const {
+            reviewId,
+            reviewerId,
+            fullName,
+            positionId,
+            positionTitle,
+            teamId,
+            teamTitle,
+        } = query;
+        return {
+            ...(reviewId ? { reviewId } : {}),
+            ...(reviewerId ? { reviewerId } : {}),
+            ...(fullName
+                ? { fullName: { contains: fullName, mode: 'insensitive' } }
+                : {}),
+            ...(positionId ? { positionId } : {}),
+            ...(positionTitle
+                ? {
+                      positionTitle: {
+                          contains: positionTitle,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(teamId ? { teamId } : {}),
+            ...(teamTitle
+                ? { teamTitle: { contains: teamTitle, mode: 'insensitive' } }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: ReviewerSearchQuery,
+    ): Prisma.ReviewerOrderByWithRelationInput[] {
+        const field = query.sortBy ?? ReviewerSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/application/events/index.html b/apps/api/test/integration/coverage/lcov-report/identity/application/events/index.html new file mode 100644 index 00000000..94876d0e --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/application/events/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for identity/application/events + + + + + + + + + +
+
+

All files identity/application/events

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
user-created.event.ts +
+
100%3/3100%0/0100%1/1100%3/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/application/events/user-created.event.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/application/events/user-created.event.ts.html new file mode 100644 index 00000000..57651555 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/application/events/user-created.event.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for identity/application/events/user-created.event.ts + + + + + + + + + +
+
+

All files / identity/application/events user-created.event.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +  +  +39x +  +393x +393x +  +  + 
/**
+ * Domain event emitted when a new user is created via the identity
+ * service (HR-driven flow). Listeners (e.g. notifications) can react
+ * by sending a welcome email.
+ */
+export class UserCreatedEvent {
+    constructor(
+        public readonly userId: number,
+        public readonly email: string,
+    ) {}
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/application/ports/index.html b/apps/api/test/integration/coverage/lcov-report/identity/application/ports/index.html new file mode 100644 index 00000000..e9beee62 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/application/ports/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for identity/application/ports + + + + + + + + + +
+
+

All files identity/application/ports

+
+ +
+ 100% + Statements + 2/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 2/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
role.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
user.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/application/ports/role.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/application/ports/role.repository.port.ts.html new file mode 100644 index 00000000..0863e861 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/application/ports/role.repository.port.ts.html @@ -0,0 +1,112 @@ + + + + + + Code coverage report for identity/application/ports/role.repository.port.ts + + + + + + + + + +
+
+

All files / identity/application/ports role.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10  +  +  +39x +  +  +  +  +  + 
import { IdentityRole } from '@intra/shared-kernel';
+import { RoleDomain } from '../../domain/role.domain';
+ 
+export const IDENTITY_ROLE_REPOSITORY = Symbol('IDENTITY.ROLE_REPOSITORY');
+ 
+export interface RoleRepositoryPort {
+    getAll(): Promise<RoleDomain[]>;
+    findByCodes(codes: IdentityRole[]): Promise<RoleDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/application/ports/user.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/application/ports/user.repository.port.ts.html new file mode 100644 index 00000000..a2329ff0 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/application/ports/user.repository.port.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for identity/application/ports/user.repository.port.ts + + + + + + + + + +
+
+

All files / identity/application/ports user.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +  +  +  +  +  +39x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    IdentityRole,
+    UpdateUserPayload,
+    UserSearchQuery,
+} from '@intra/shared-kernel';
+import { UserDomain } from '../../domain/user.domain';
+ 
+export const IDENTITY_USER_REPOSITORY = Symbol('IDENTITY.USER_REPOSITORY');
+ 
+export interface UserRepositoryPort {
+    create(user: UserDomain): Promise<UserDomain>;
+    findById(
+        id: number,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain | null>;
+    findByEmail(
+        email: string,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain | null>;
+    search(query: UserSearchQuery): Promise<UserDomain[]>;
+    updateById(id: number, patch: UpdateUserPayload): Promise<UserDomain>;
+    deleteById(id: number): Promise<void>;
+    replaceRoles(userId: number, roles: IdentityRole[]): Promise<UserDomain>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/application/services/identity-role.service.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/application/services/identity-role.service.ts.html new file mode 100644 index 00000000..0c503805 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/application/services/identity-role.service.ts.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for identity/application/services/identity-role.service.ts + + + + + + + + + +
+
+

All files / identity/application/services identity-role.service.ts

+
+ +
+ 100% + Statements + 8/8 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22  +57x +  +  +57x +  +  +57x +  +  +57x +  +  +  +3x +  +  +  +3x +  +  + 
import { IdentityRole } from '@intra/shared-kernel';
+import { Inject, Injectable } from '@nestjs/common';
+import { RoleDomain } from '../../domain/role.domain';
+import type { RoleRepositoryPort } from '../ports/role.repository.port';
+import { IDENTITY_ROLE_REPOSITORY } from '../ports/role.repository.port';
+ 
+@Injectable()
+export class IdentityRoleService {
+    constructor(
+        @Inject(IDENTITY_ROLE_REPOSITORY)
+        private readonly roles: RoleRepositoryPort,
+    ) {}
+ 
+    async list(): Promise<RoleDomain[]> {
+        return this.roles.getAll();
+    }
+ 
+    async getByCodes(codes: IdentityRole[]): Promise<RoleDomain[]> {
+        return this.roles.findByCodes(codes);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/application/services/identity-user.service.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/application/services/identity-user.service.ts.html new file mode 100644 index 00000000..a70999df --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/application/services/identity-user.service.ts.html @@ -0,0 +1,682 @@ + + + + + + Code coverage report for identity/application/services/identity-user.service.ts + + + + + + + + + +
+
+

All files / identity/application/services identity-user.service.ts

+
+ +
+ 90.74% + Statements + 49/54 +
+ + +
+ 88.37% + Branches + 38/43 +
+ + +
+ 83.33% + Functions + 10/12 +
+ + +
+ 93.87% + Lines + 46/49 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +20057x +  +  +  +  +  +  +57x +  +  +  +  +  +57x +57x +57x +  +57x +  +57x +  +  +  +  +57x +  +  +57x +  +57x +57x +  +  +  +671x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +671x +  +  +  +671x +1x +  +  +  +  +670x +  +670x +670x +  +  +  +  +  +670x +  +  +  +2x +  +  +  +  +  +  +27x +27x +21x +  +  +  +  +  +  +2x +  +  +  +3x +  +2x +  +  +  +2x +  +  +  +  +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +1x +  +  +  +  +  +  +3x +  +3x +3x +3x +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +3x +  +  +  +3x +1x +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +2x +2x +  +  +  +  +  +2x +  +  +  +  +  +  +  +673x +  +  +673x +  +  + 
import {
+    CreateUserPayload,
+    IdentityRole,
+    IdentityStatus,
+    UpdateUserPayload,
+    UserSearchQuery,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { EventEmitter2 } from '@nestjs/event-emitter';
+import { UserDomain } from '../../domain/user.domain';
+import { UserCreatedEvent } from '../events/user-created.event';
+import type { RoleRepositoryPort } from '../ports/role.repository.port';
+import { IDENTITY_ROLE_REPOSITORY } from '../ports/role.repository.port';
+import type { UserRepositoryPort } from '../ports/user.repository.port';
+import { IDENTITY_USER_REPOSITORY } from '../ports/user.repository.port';
+ 
+export type UpdateUserCommand = Partial<Omit<CreateUserPayload, 'email'>>;
+ 
+@Injectable()
+export class IdentityUserService {
+    constructor(
+        @Inject(IDENTITY_USER_REPOSITORY)
+        private readonly users: UserRepositoryPort,
+        @Inject(IDENTITY_ROLE_REPOSITORY)
+        private readonly roles: RoleRepositoryPort,
+        private readonly eventEmitter: EventEmitter2,
+    ) {}
+ 
+    async create(payload: CreateUserPayload): Promise<UserDomain> {
+        const user = UserDomain.create({
+            firstName: payload.firstName,
+            secondName: payload.secondName,
+            lastName: payload.lastName,
+            fullName:
+                payload.fullName ??
+                this.buildFullName(
+                    payload.lastName,
+                    payload.firstName,
+                    payload.secondName,
+                ),
+            email: payload.email,
+            avatarUrl: payload.avatarUrl,
+            status: payload.status ?? IdentityStatus.ACTIVE,
+            positionId: payload.positionId ?? null,
+            teamId: payload.teamId ?? null,
+            managerId: payload.managerId ?? null,
+            roles: payload.roles ?? [],
+        });
+ 
+        const existing = await this.users.findByEmail(payload.email, {
+            withRoles: true,
+        });
+ 
+        if (existing) {
+            throw new BadRequestException(
+                `User with email ${payload.email} already exists`,
+            );
+        }
+ 
+        const created = await this.users.create(user);
+ 
+        Eif (created.id) {
+            this.eventEmitter.emit(
+                'user.created',
+                new UserCreatedEvent(created.id, created.email),
+            );
+        }
+ 
+        return created;
+    }
+ 
+    async search(query: UserSearchQuery): Promise<UserDomain[]> {
+        return this.users.search(query);
+    }
+ 
+    async getById(
+        id: number,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain> {
+        const user = await this.users.findById(id, opts);
+        if (!user) throw new NotFoundException('User not found');
+        return user;
+    }
+ 
+    async findByEmail(
+        email: string,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain | null> {
+        return this.users.findByEmail(email, opts);
+    }
+ 
+    async update(id: number, patch: UpdateUserCommand): Promise<UserDomain> {
+        const current = await this.getById(id);
+        const shouldUpdateFullName =
+            patch.firstName !== undefined ||
+            patch.secondName !== undefined ||
+            patch.lastName !== undefined;
+ 
+        const fullName = shouldUpdateFullName
+            ? this.buildFullName(
+                  patch.lastName ?? current.lastName,
+                  patch.firstName ?? current.firstName,
+                  patch.secondName ?? current.secondName,
+              )
+            : undefined;
+ 
+        const payload: UpdateUserPayload = {
+            ...patch,
+            ...(fullName !== undefined ? { fullName } : {}),
+        };
+ 
+        return this.users.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        await this.users.deleteById(id);
+    }
+ 
+    async replaceRoles(
+        userId: number,
+        roles: IdentityRole[],
+    ): Promise<UserDomain> {
+        await this.getById(userId);
+ 
+        const uniqueRoles = Array.from(new Set(roles));
+        const found = await this.roles.findByCodes(uniqueRoles);
+        Iif (found.length !== uniqueRoles.length) {
+            const foundCodes = new Set(found.map((r) => r.code));
+            const missing = uniqueRoles.filter((c) => !foundCodes.has(c));
+            throw new BadRequestException(
+                `Unknown roles: ${missing.join(', ')}`,
+            );
+        }
+ 
+        return this.users.replaceRoles(userId, uniqueRoles);
+    }
+ 
+    async upsertExternalUser(payload: {
+        email: string;
+        firstName: string;
+        lastName: string;
+        secondName: string | undefined;
+        avatarUrl?: string;
+    }): Promise<UserDomain> {
+        const existing = await this.users.findByEmail(payload.email, {
+            withRoles: true,
+        });
+ 
+        if (existing) {
+            return existing;
+        }
+ 
+        const createdUser = UserDomain.create({
+            firstName: payload.firstName,
+            secondName: payload.secondName,
+            lastName: payload.lastName,
+            fullName: this.buildFullName(
+                payload.lastName,
+                payload.firstName,
+                payload.secondName,
+            ),
+            email: payload.email,
+            avatarUrl: payload.avatarUrl,
+            status: IdentityStatus.ACTIVE,
+            positionId: null,
+            teamId: null,
+            managerId: null,
+            roles: [IdentityRole.EMPLOYEE],
+        });
+ 
+        const persisted = await this.users.create(createdUser);
+ 
+        Eif (persisted.id) {
+            this.eventEmitter.emit(
+                'user.created',
+                new UserCreatedEvent(persisted.id, persisted.email),
+            );
+        }
+ 
+        return persisted;
+    }
+ 
+    private buildFullName(
+        lastName: string,
+        firstName: string,
+        secondName: string | undefined,
+    ): string {
+        const parts = [lastName, firstName, secondName ?? undefined].filter(
+            Boolean,
+        );
+        return parts.join(' ');
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/application/services/index.html b/apps/api/test/integration/coverage/lcov-report/identity/application/services/index.html new file mode 100644 index 00000000..1982e3e7 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/application/services/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for identity/application/services + + + + + + + + + +
+
+

All files identity/application/services

+
+ +
+ 91.93% + Statements + 57/62 +
+ + +
+ 88.37% + Branches + 38/43 +
+ + +
+ 86.66% + Functions + 13/15 +
+ + +
+ 94.54% + Lines + 52/55 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
identity-role.service.ts +
+
100%8/8100%0/0100%3/3100%6/6
identity-user.service.ts +
+
90.74%49/5488.37%38/4383.33%10/1293.87%46/49
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/index.html b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/index.html new file mode 100644 index 00000000..0a293aae --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for identity/infrastructure/mappers + + + + + + + + + +
+
+

All files identity/infrastructure/mappers

+
+ +
+ 93.33% + Statements + 14/15 +
+ + +
+ 100% + Branches + 8/8 +
+ + +
+ 88.88% + Functions + 8/9 +
+ + +
+ 93.33% + Lines + 14/15 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
role.mapper.ts +
+
80%4/5100%0/066.66%2/380%4/5
user.mapper.ts +
+
100%10/10100%8/8100%6/6100%10/10
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/role.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/role.mapper.ts.html new file mode 100644 index 00000000..6c7e3df9 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/role.mapper.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for identity/infrastructure/mappers/role.mapper.ts + + + + + + + + + +
+
+

All files / identity/infrastructure/mappers role.mapper.ts

+
+ +
+ 80% + Statements + 4/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 66.66% + Functions + 2/3 +
+ + +
+ 80% + Lines + 4/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +39x +  +39x +  +34x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +72x +  +  + 
import { IdentityRole as PrismaIdentityRole, Role } from '@intra/database';
+import { IdentityRole } from '@intra/shared-kernel';
+import { RoleDomain } from '../../domain/role.domain';
+ 
+export class RoleMapper {
+    static toDomain(role: Role): RoleDomain {
+        return new RoleDomain({
+            id: role.id,
+            code: RoleMapper.fromPrismaRole(role.code),
+            title: role.title,
+            description: role.description,
+            createdAt: role.createdAt,
+            updatedAt: role.updatedAt,
+        });
+    }
+ 
+    static toPrismaRole(domainRole: IdentityRole): PrismaIdentityRole {
+        return domainRole.toString().toUpperCase() as PrismaIdentityRole;
+    }
+ 
+    static fromPrismaRole(prismaRole: PrismaIdentityRole): IdentityRole {
+        return prismaRole.toString().toUpperCase() as IdentityRole;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/user.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/user.mapper.ts.html new file mode 100644 index 00000000..1af9f129 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/mappers/user.mapper.ts.html @@ -0,0 +1,304 @@ + + + + + + Code coverage report for identity/infrastructure/mappers/user.mapper.ts + + + + + + + + + +
+
+

All files / identity/infrastructure/mappers user.mapper.ts

+
+ +
+ 100% + Statements + 10/10 +
+ + +
+ 100% + Branches + 8/8 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 10/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74  +  +  +  +  +  +  +  +39x +39x +  +  +  +  +  +39x +  +472x +38x +  +  +472x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +420x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +21x +  +  +  +  +  +  +  +  +  +  +420x +  +  +  +474x +  +  + 
import {
+    Prisma,
+    IdentityStatus as PrismaIdentityStatus,
+    Role,
+    User,
+    UserRole,
+} from '@intra/database';
+import { IdentityStatus } from '@intra/shared-kernel';
+import { UserDomain } from '../../domain/user.domain';
+import { RoleMapper } from './role.mapper';
+ 
+export type UserWithRoles = User & {
+    userRoles?: (UserRole & { role?: Role | null })[];
+};
+ 
+export class UserMapper {
+    static toDomain(user: UserWithRoles): UserDomain {
+        const roles = (user.userRoles ?? []).map((relation) =>
+            RoleMapper.fromPrismaRole(relation.roleCode),
+        );
+ 
+        return UserDomain.create({
+            id: user.id,
+            firstName: user.firstName,
+            secondName: user.secondName ?? undefined,
+            lastName: user.lastName,
+            fullName: user.fullName,
+            email: user.email,
+            avatarUrl: user.avatarUrl,
+            status: UserMapper.toDomainStatus(user.status),
+            positionId: user.positionId,
+            teamId: user.teamId,
+            managerId: user.managerId,
+            createdAt: user.createdAt,
+            updatedAt: user.updatedAt,
+            roles,
+        });
+    }
+ 
+    static toPrisma(user: UserDomain): Prisma.UserUncheckedCreateInput {
+        return {
+            firstName: user.firstName,
+            secondName: user.secondName,
+            lastName: user.lastName,
+            fullName: user.fullName,
+            email: user.email,
+            avatarUrl: user.avatarUrl,
+            status: UserMapper.toPrismaStatus(user.status),
+            positionId: user.positionId ?? null,
+            teamId: user.teamId,
+            managerId: user.managerId,
+            ...(user.roles.length
+                ? {
+                      userRoles: {
+                          createMany: {
+                              data: user.roles.map((roleCode) => ({
+                                  roleCode,
+                              })),
+                          },
+                      },
+                  }
+                : {}),
+        };
+    }
+ 
+    static toPrismaStatus(domainStatus: IdentityStatus): PrismaIdentityStatus {
+        return domainStatus.toString().toUpperCase() as PrismaIdentityStatus;
+    }
+ 
+    static toDomainStatus(prismaStatus: PrismaIdentityStatus): IdentityStatus {
+        return prismaStatus.toString().toUpperCase() as IdentityStatus;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/index.html b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/index.html new file mode 100644 index 00000000..1a3406bb --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for identity/infrastructure/prisma-repositories + + + + + + + + + +
+
+

All files identity/infrastructure/prisma-repositories

+
+ +
+ 98.27% + Statements + 57/58 +
+ + +
+ 79.62% + Branches + 43/54 +
+ + +
+ 100% + Functions + 19/19 +
+ + +
+ 97.95% + Lines + 48/49 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
role.repository.ts +
+
100%13/1383.33%5/6100%3/3100%10/10
user.repository.ts +
+
97.77%44/4579.16%38/48100%16/1697.43%38/39
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/role.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/role.repository.ts.html new file mode 100644 index 00000000..c3c1bb72 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/role.repository.ts.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for identity/infrastructure/prisma-repositories/role.repository.ts + + + + + + + + + +
+
+

All files / identity/infrastructure/prisma-repositories role.repository.ts

+
+ +
+ 100% + Statements + 13/13 +
+ + +
+ 83.33% + Branches + 5/6 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 10/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26  +57x +57x +  +57x +  +  +57x +57x +  +  +6x +  +  +6x +  +  +  +9x +6x +  +  +6x +  +  + 
import { IdentityRole } from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { RoleRepositoryPort } from '../../application/ports/role.repository.port';
+import { RoleMapper } from '../mappers/role.mapper';
+ 
+@Injectable()
+export class RoleRepository implements RoleRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async getAll() {
+        const roles = await this.prisma.role.findMany({
+            orderBy: { code: 'asc' },
+        });
+        return roles.map(RoleMapper.toDomain);
+    }
+ 
+    async findByCodes(codes: IdentityRole[]) {
+        if (!codes.length) return [];
+        const roles = await this.prisma.role.findMany({
+            where: { code: { in: codes } },
+        });
+        return roles.map(RoleMapper.toDomain);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/user.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/user.repository.ts.html new file mode 100644 index 00000000..cc7a1f17 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/identity/infrastructure/prisma-repositories/user.repository.ts.html @@ -0,0 +1,682 @@ + + + + + + Code coverage report for identity/infrastructure/prisma-repositories/user.repository.ts + + + + + + + + + +
+
+

All files / identity/infrastructure/prisma-repositories user.repository.ts

+
+ +
+ 97.77% + Statements + 44/45 +
+ + +
+ 79.16% + Branches + 38/48 +
+ + +
+ 100% + Functions + 16/16 +
+ + +
+ 97.43% + Lines + 38/39 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200  +57x +  +  +  +  +  +  +57x +57x +  +  +57x +  +  +57x +57x +  +  +742x +  +  +  +  +742x +  +  +  +  +  +  +56x +  +  +  +  +56x +  +  +  +  +  +  +678x +  +  +  +  +678x +  +  +  +8x +8x +  +8x +  +  +  +  +  +13x +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +4x +  +  +  +2x +  +  +  +  +  +  +8x +8x +  +  +  +8x +  +8x +8x +11x +  +8x +3x +5x +  +  +  +  +  +8x +8x +  +  +8x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +  +8x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +8x +8x +  +  +  +1488x +1439x +  +  +  +  +  +  + 
import { Prisma } from '@intra/database';
+import {
+    IdentityRole,
+    SortDirection,
+    UpdateUserPayload,
+    UserSearchQuery,
+    UserSortField,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { UserRepositoryPort } from '../../application/ports/user.repository.port';
+import { UserDomain } from '../../domain/user.domain';
+import { UserMapper, UserWithRoles } from '../mappers/user.mapper';
+ 
+@Injectable()
+export class UserRepository implements UserRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(user: UserDomain): Promise<UserDomain> {
+        const created = await this.prisma.user.create({
+            data: UserMapper.toPrisma(user),
+            include: this.withRolesInclude(true),
+        });
+ 
+        return UserMapper.toDomain(created);
+    }
+ 
+    async findById(
+        id: number,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain | null> {
+        const user = await this.prisma.user.findUnique({
+            where: { id },
+            include: this.withRolesInclude(opts?.withRoles),
+        });
+ 
+        return user ? UserMapper.toDomain(user) : null;
+    }
+ 
+    async findByEmail(
+        email: string,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain | null> {
+        const user = await this.prisma.user.findUnique({
+            where: { email },
+            include: this.withRolesInclude(opts?.withRoles),
+        });
+ 
+        return user ? UserMapper.toDomain(user) : null;
+    }
+ 
+    async search(query: UserSearchQuery): Promise<UserDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+ 
+        const items = await this.prisma.user.findMany({
+            where,
+            orderBy,
+            include: this.withRolesInclude(true),
+        });
+ 
+        return items.map((u) => UserMapper.toDomain(u as UserWithRoles));
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateUserPayload,
+    ): Promise<UserDomain> {
+        const updated = await this.prisma.user.update({
+            where: { id },
+            data: {
+                ...patch,
+                secondName: patch.secondName ?? null,
+                teamId: patch.teamId ?? null,
+                managerId: patch.managerId ?? null,
+            },
+            include: this.withRolesInclude(true),
+        });
+ 
+        return UserMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.user.delete({ where: { id } });
+    }
+ 
+    async replaceRoles(
+        userId: number,
+        roles: IdentityRole[],
+    ): Promise<UserDomain> {
+        await this.prisma.$transaction(async (tx) => {
+            const deleteWhere: Prisma.UserRoleWhereInput = roles.length
+                ? { userId, NOT: { roleCode: { in: roles } } }
+                : { userId };
+ 
+            await tx.userRole.deleteMany({ where: deleteWhere });
+ 
+            const existing = await tx.userRole.findMany({ where: { userId } });
+            const existingCodes = new Set(existing.map((r) => r.roleCode));
+            const toCreate = roles.filter((role) => !existingCodes.has(role));
+ 
+            if (toCreate.length) {
+                await tx.userRole.createMany({
+                    data: toCreate.map((roleCode) => ({ userId, roleCode })),
+                    skipDuplicates: true,
+                });
+            }
+        });
+ 
+        const refreshed = await this.findById(userId, { withRoles: true });
+        Iif (!refreshed) {
+            throw new Error('User not found after roles update');
+        }
+        return refreshed;
+    }
+ 
+    private buildWhere(query: UserSearchQuery): Prisma.UserWhereInput {
+        const {
+            firstName,
+            secondName,
+            lastName,
+            fullName,
+            search,
+            email,
+            status,
+            teamId,
+            positionId,
+            managerId,
+            roles,
+        } = query;
+ 
+        return {
+            ...(firstName
+                ? { firstName: { contains: firstName, mode: 'insensitive' } }
+                : {}),
+            ...(secondName
+                ? { secondName: { contains: secondName, mode: 'insensitive' } }
+                : {}),
+            ...(lastName
+                ? { lastName: { contains: lastName, mode: 'insensitive' } }
+                : {}),
+            ...(fullName
+                ? { fullName: { contains: fullName, mode: 'insensitive' } }
+                : {}),
+            ...(search
+                ? {
+                      OR: [
+                          {
+                              firstName: {
+                                  contains: search,
+                                  mode: 'insensitive',
+                              },
+                          },
+                          {
+                              lastName: {
+                                  contains: search,
+                                  mode: 'insensitive',
+                              },
+                          },
+                          { email: { contains: search, mode: 'insensitive' } },
+                          {
+                              fullName: {
+                                  contains: search,
+                                  mode: 'insensitive',
+                              },
+                          },
+                      ],
+                  }
+                : {}),
+            ...(email
+                ? { email: { contains: email, mode: 'insensitive' } }
+                : {}),
+            ...(status ? { status } : {}),
+            ...(teamId ? { teamId } : {}),
+            ...(positionId !== undefined ? { positionId } : {}),
+            ...(managerId ? { managerId } : {}),
+            ...(roles?.length
+                ? { userRoles: { some: { roleCode: { in: roles } } } }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: UserSearchQuery,
+    ): Prisma.UserOrderByWithRelationInput[] {
+        const field = query.sortBy ?? UserSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+ 
+    private withRolesInclude(withRoles?: boolean) {
+        if (!withRoles) return undefined;
+        return {
+            userRoles: {
+                select: { roleCode: true },
+            },
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/index.html b/apps/api/test/integration/coverage/lcov-report/index.html new file mode 100644 index 00000000..32f6c531 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/index.html @@ -0,0 +1,326 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 90.21% + Statements + 2359/2615 +
+ + +
+ 68.53% + Branches + 1568/2288 +
+ + +
+ 92.47% + Functions + 504/545 +
+ + +
+ 90.87% + Lines + 2200/2421 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
feedback360/application/listeners +
+
94.26%115/12276.31%87/114100%11/1193.85%107/114
feedback360/application/services +
+
78.69%362/46065.02%290/44682.66%62/7581.35%349/429
feedback360/infrastructure/prisma-repositories +
+
100%279/27973.02%203/278100%79/79100%254/254
identity/application/services +
+
91.93%57/6288.37%38/4386.66%13/1594.54%52/55
identity/infrastructure/prisma-repositories +
+
98.27%57/5879.62%43/54100%19/1997.95%48/49
library/application/services +
+
98.08%154/15769.53%89/128100%38/3897.9%140/143
library/infrastructure/prisma-repositories +
+
93.29%153/16475.47%80/10694.23%49/5292.76%141/152
notifications/application/listeners +
+
90.76%59/6561.81%34/55100%6/689.83%53/59
notifications/application/services +
+
83.87%104/12463.54%61/96100%9/983.6%102/122
notifications/infrastructure/prisma-repositories +
+
100%19/1990%9/10100%7/7100%17/17
organisation/application/services +
+
98.97%97/9884.09%37/44100%25/2598.86%87/88
organisation/infrastructure/prisma-repositories +
+
97.56%80/8277.77%42/54100%24/2497.29%72/74
reporting/application/listeners +
+
90.38%47/5271.42%30/4280%4/589.58%43/48
reporting/application/services +
+
86.93%639/73563.53%413/65085.1%120/14188.31%612/693
reporting/infrastructure/prisma-repositories +
+
99.27%137/13866.66%112/16897.43%38/3999.19%123/124
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/ports/cluster.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/ports/cluster.repository.port.ts.html new file mode 100644 index 00000000..129b2f78 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/ports/cluster.repository.port.ts.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for library/application/ports/cluster.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports cluster.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +  +28x +  +  +  +  +  +  +  +  + 
import { ClusterSearchQuery, UpdateClusterPayload } from '@intra/shared-kernel';
+import { ClusterDomain } from '../../domain/cluster.domain';
+ 
+export const CLUSTER_REPOSITORY = Symbol('LIBRARY.CLUSTER_REPOSITORY');
+ 
+export interface ClusterRepositoryPort {
+    create(cluster: ClusterDomain): Promise<ClusterDomain>;
+    findById(id: number): Promise<ClusterDomain | null>;
+    search(query: ClusterSearchQuery): Promise<ClusterDomain[]>;
+    updateById(id: number, patch: UpdateClusterPayload): Promise<ClusterDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/ports/competence-question-template-relation.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/ports/competence-question-template-relation.repository.port.ts.html new file mode 100644 index 00000000..c73ef760 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/ports/competence-question-template-relation.repository.port.ts.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for library/application/ports/competence-question-template-relation.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports competence-question-template-relation.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26  +  +9x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { CompetenceQuestionTemplateRelationDomain } from '../../domain/competence-question-template-relation.domain';
+ 
+export const COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY = Symbol(
+    'LIBRARY.COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY',
+);
+ 
+export interface CompetenceQuestionTemplateRelationRepositoryPort {
+    link(
+        competenceId: number,
+        questionTemplateId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain>;
+    unlink(competenceId: number, questionTemplateId: number): Promise<void>;
+    listByCompetence(
+        competenceId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]>;
+    listByQuestionTemplate(
+        questionTemplateId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]>;
+    replaceForCompetence(
+        competenceId: number,
+        questionTemplateIds: number[],
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]>;
+    deleteAllForQuestionTemplate(questionTemplateId: number): Promise<void>;
+    deleteAllForCompetence(competenceId: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/ports/competence.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/ports/competence.repository.port.ts.html new file mode 100644 index 00000000..0edefe1c --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/ports/competence.repository.port.ts.html @@ -0,0 +1,139 @@ + + + + + + Code coverage report for library/application/ports/competence.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports competence.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19  +  +  +  +  +  +28x +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    CompetenceSearchQuery,
+    UpdateCompetencePayload,
+} from '@intra/shared-kernel';
+import { CompetenceDomain } from '../../domain/competence.domain';
+ 
+export const COMPETENCE_REPOSITORY = Symbol('LIBRARY.COMPETENCE_REPOSITORY');
+ 
+export interface CompetenceRepositoryPort {
+    create(competence: CompetenceDomain): Promise<CompetenceDomain>;
+    findById(id: number): Promise<CompetenceDomain | null>;
+    search(query: CompetenceSearchQuery): Promise<CompetenceDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdateCompetencePayload,
+    ): Promise<CompetenceDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/ports/index.html b/apps/api/test/integration/coverage/lcov-report/library/application/ports/index.html new file mode 100644 index 00000000..14d3689e --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/ports/index.html @@ -0,0 +1,191 @@ + + + + + + Code coverage report for library/application/ports + + + + + + + + + +
+
+

All files library/application/ports

+
+ +
+ 100% + Statements + 6/6 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cluster.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
competence-question-template-relation.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
competence.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
position-competence-relation.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
position-question-template-relation.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
question-template.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/ports/position-competence-relation.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/ports/position-competence-relation.repository.port.ts.html new file mode 100644 index 00000000..5610f1dd --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/ports/position-competence-relation.repository.port.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for library/application/ports/position-competence-relation.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports position-competence-relation.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +15x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { PositionCompetenceRelationDomain } from '../../domain/position-competence-relation.domain';
+ 
+export const POSITION_COMPETENCE_RELATION_REPOSITORY = Symbol(
+    'LIBRARY.POSITION_COMPETENCE_RELATION_REPOSITORY',
+);
+ 
+export interface PositionCompetenceRelationRepositoryPort {
+    link(
+        competenceId: number,
+        positionId: number,
+    ): Promise<PositionCompetenceRelationDomain>;
+    unlink(competenceId: number, positionId: number): Promise<void>;
+    listByCompetence(
+        competenceId: number,
+    ): Promise<PositionCompetenceRelationDomain[]>;
+    listByPosition(
+        positionId: number,
+    ): Promise<PositionCompetenceRelationDomain[]>;
+    replaceForCompetence(
+        competenceId: number,
+        positionIds: number[],
+    ): Promise<PositionCompetenceRelationDomain[]>;
+    deleteAllForCompetence(competenceId: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/ports/position-question-template-relation.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/ports/position-question-template-relation.repository.port.ts.html new file mode 100644 index 00000000..d561ea5f --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/ports/position-question-template-relation.repository.port.ts.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for library/application/ports/position-question-template-relation.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports position-question-template-relation.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22  +  +9x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { PositionQuestionTemplateRelationDomain } from '../../domain/position-question-template-relation.domain';
+ 
+export const POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY = Symbol(
+    'LIBRARY.POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY',
+);
+ 
+export interface PositionQuestionTemplateRelationRepositoryPort {
+    link(
+        questionId: number,
+        positionId: number,
+    ): Promise<PositionQuestionTemplateRelationDomain>;
+    unlink(questionId: number, positionId: number): Promise<void>;
+    listByQuestion(
+        questionId: number,
+    ): Promise<PositionQuestionTemplateRelationDomain[]>;
+    replace(
+        questionId: number,
+        positionIds: number[],
+    ): Promise<PositionQuestionTemplateRelationDomain[]>;
+    deleteAllForQuestionTemplate(questionTemplateId: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/ports/question-template.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/ports/question-template.repository.port.ts.html new file mode 100644 index 00000000..737285f5 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/ports/question-template.repository.port.ts.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for library/application/ports/question-template.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports question-template.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23  +  +  +  +  +  +9x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    QuestionTemplateSearchQuery,
+    UpdateQuestionTemplatePayload,
+} from '@intra/shared-kernel';
+import { QuestionTemplateDomain } from '../../domain/question-template.domain';
+ 
+export const QUESTION_TEMPLATE_REPOSITORY = Symbol(
+    'LIBRARY.QUESTION_TEMPLATE_REPOSITORY',
+);
+ 
+export interface QuestionTemplateRepositoryPort {
+    create(question: QuestionTemplateDomain): Promise<QuestionTemplateDomain>;
+    findById(id: number): Promise<QuestionTemplateDomain | null>;
+    search(
+        query: QuestionTemplateSearchQuery,
+    ): Promise<QuestionTemplateDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdateQuestionTemplatePayload,
+    ): Promise<QuestionTemplateDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/services/cluster.service.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/services/cluster.service.ts.html new file mode 100644 index 00000000..e7f7985c --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/services/cluster.service.ts.html @@ -0,0 +1,382 @@ + + + + + + Code coverage report for library/application/services/cluster.service.ts + + + + + + + + + +
+
+

All files / library/application/services cluster.service.ts

+
+ +
+ 100% + Statements + 31/31 +
+ + +
+ 70.58% + Branches + 24/34 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 28/28 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100  +  +  +  +  +44x +  +  +  +  +  +44x +44x +  +  +  +44x +  +  +44x +  +  +44x +44x +  +  +  +28x +27x +  +26x +  +  +  +  +  +  +  +26x +  +  +  +8x +  +  +  +7x +7x +5x +  +  +  +  +  +  +3x +  +3x +1x +  +  +2x +1x +1x +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +2x +1x +  +  +  +29x +2x +  +  +  +  +  + 
import {
+    ClusterSearchQuery,
+    CreateClusterPayload,
+    UpdateClusterPayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { ClusterDomain } from '../../domain/cluster.domain';
+import {
+    CLUSTER_REPOSITORY,
+    ClusterRepositoryPort,
+} from '../ports/cluster.repository.port';
+import { CompetenceService } from './competence.service';
+ 
+@Injectable()
+export class ClusterService {
+    constructor(
+        @Inject(CLUSTER_REPOSITORY)
+        private readonly clusters: ClusterRepositoryPort,
+        private readonly competences: CompetenceService,
+    ) {}
+ 
+    async create(payload: CreateClusterPayload): Promise<ClusterDomain> {
+        await this.ensureBounds(payload.lowerBound, payload.upperBound);
+        await this.competences.getById(payload.competenceId);
+ 
+        const cluster = ClusterDomain.create({
+            competenceId: payload.competenceId,
+            lowerBound: payload.lowerBound,
+            upperBound: payload.upperBound,
+            title: payload.title,
+            description: payload.description,
+        });
+ 
+        return this.clusters.create(cluster);
+    }
+ 
+    async search(query: ClusterSearchQuery): Promise<ClusterDomain[]> {
+        return this.clusters.search(query);
+    }
+ 
+    async getById(id: number): Promise<ClusterDomain> {
+        const cluster = await this.clusters.findById(id);
+        if (!cluster) throw new NotFoundException('Cluster not found');
+        return cluster;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateClusterPayload,
+    ): Promise<ClusterDomain> {
+        const current = await this.getById(id);
+ 
+        if (patch.competenceId && patch.competenceId !== current.competenceId) {
+            await this.competences.getById(patch.competenceId);
+        }
+ 
+        if (patch.lowerBound !== undefined || patch.upperBound !== undefined) {
+            const lower = patch.lowerBound ?? current.lowerBound;
+            const upper = patch.upperBound ?? current.upperBound;
+            await this.ensureBounds(lower, upper);
+        }
+ 
+        const payload: UpdateClusterPayload = {
+            ...(patch.competenceId !== undefined
+                ? { competenceId: patch.competenceId }
+                : {}),
+            ...(patch.lowerBound !== undefined
+                ? { lowerBound: patch.lowerBound }
+                : {}),
+            ...(patch.upperBound !== undefined
+                ? { upperBound: patch.upperBound }
+                : {}),
+            ...(patch.title !== undefined ? { title: patch.title } : {}),
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+        };
+ 
+        return this.clusters.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        await this.clusters.deleteById(id);
+    }
+ 
+    private async ensureBounds(lower: number, upper: number): Promise<void> {
+        if (lower > upper) {
+            throw new BadRequestException(
+                'Lower bound must be less than or equal to upper bound',
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/services/competence.service.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/services/competence.service.ts.html new file mode 100644 index 00000000..8fc0cd88 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/services/competence.service.ts.html @@ -0,0 +1,601 @@ + + + + + + Code coverage report for library/application/services/competence.service.ts + + + + + + + + + +
+
+

All files / library/application/services competence.service.ts

+
+ +
+ 100% + Statements + 56/56 +
+ + +
+ 68.42% + Branches + 26/38 +
+ + +
+ 100% + Functions + 15/15 +
+ + +
+ 100% + Lines + 50/50 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173  +  +  +  +  +44x +  +  +  +  +  +44x +44x +44x +  +  +  +44x +  +  +  +44x +  +  +  +44x +  +  +  +  +  +44x +  +  +44x +  +44x +  +44x +  +44x +44x +  +  +  +102x +  +  +  +  +102x +  +  +  +3x +  +  +  +114x +114x +106x +  +  +  +  +  +  +2x +1x +  +  +  +  +  +  +1x +  +  +  +4x +3x +3x +3x +  +  +  +2x +  +  +  +  +  +  +  +  +  +1x +1x +  +  +1x +  +  +  +  +  +  +5x +5x +  +4x +4x +  +  +  +  +  +  +2x +2x +  +  +  +8x +  +8x +  +  +8x +  +  +  +  +  +  +3x +3x +  +2x +  +  +  +2x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +6x +  +6x +  +  +6x +  +  +  +  +  +3x +3x +  +  + 
import {
+    CompetenceSearchQuery,
+    CreateCompetencePayload,
+    UpdateCompetencePayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { PositionService } from 'src/contexts/organisation/application/services/position.service';
+import { CompetenceDomain } from '../../domain/competence.domain';
+import {
+    COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY,
+    CompetenceQuestionTemplateRelationRepositoryPort,
+} from '../ports/competence-question-template-relation.repository.port';
+import {
+    COMPETENCE_REPOSITORY,
+    CompetenceRepositoryPort,
+} from '../ports/competence.repository.port';
+import {
+    POSITION_COMPETENCE_RELATION_REPOSITORY,
+    PositionCompetenceRelationRepositoryPort,
+} from '../ports/position-competence-relation.repository.port';
+import {
+    QUESTION_TEMPLATE_REPOSITORY,
+    QuestionTemplateRepositoryPort,
+} from '../ports/question-template.repository.port';
+ 
+@Injectable()
+export class CompetenceService {
+    constructor(
+        @Inject(COMPETENCE_REPOSITORY)
+        private readonly competences: CompetenceRepositoryPort,
+        @Inject(QUESTION_TEMPLATE_REPOSITORY)
+        private readonly questionTemplates: QuestionTemplateRepositoryPort,
+        @Inject(POSITION_COMPETENCE_RELATION_REPOSITORY)
+        private readonly positionCompetenceRelations: PositionCompetenceRelationRepositoryPort,
+        @Inject(COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY)
+        private readonly competenceQuestionTemplateRelations: CompetenceQuestionTemplateRelationRepositoryPort,
+        private readonly positions: PositionService,
+    ) {}
+ 
+    async create(payload: CreateCompetencePayload): Promise<CompetenceDomain> {
+        const competence = CompetenceDomain.create({
+            code: payload.code ?? null,
+            title: payload.title,
+            description: payload.description ?? null,
+        });
+        return this.competences.create(competence);
+    }
+ 
+    async search(query: CompetenceSearchQuery): Promise<CompetenceDomain[]> {
+        return this.competences.search(query);
+    }
+ 
+    async getById(id: number): Promise<CompetenceDomain> {
+        const competence = await this.competences.findById(id);
+        if (!competence) throw new NotFoundException('Competence not found');
+        return competence;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateCompetencePayload,
+    ): Promise<CompetenceDomain> {
+        await this.getById(id);
+        const payload: UpdateCompetencePayload = {
+            ...(patch.code !== undefined ? { code: patch.code } : {}),
+            ...(patch.title !== undefined ? { title: patch.title } : {}),
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+        };
+        return this.competences.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        const relatedQuestionTemplates = await this.listQuestionTemplates(id);
+        const relatedPositions = await this.listPositions(id);
+        if (
+            relatedQuestionTemplates.length > 0 ||
+            relatedPositions.length > 0
+        ) {
+            throw new BadRequestException(
+                'Competence #' +
+                    id +
+                    ' cannot be deleted. It has ' +
+                    relatedQuestionTemplates.length +
+                    ' question templates and ' +
+                    relatedPositions.length +
+                    ' positions assigned to it.',
+            );
+        }
+        await this.positionCompetenceRelations.deleteAllForCompetence(id);
+        await this.competenceQuestionTemplateRelations.deleteAllForCompetence(
+            id,
+        );
+        await this.competences.deleteById(id);
+    }
+ 
+    async attachPosition(
+        competenceId: number,
+        positionId: number,
+    ): Promise<number[]> {
+        await this.getById(competenceId);
+        await this.positions.getById(positionId);
+ 
+        await this.positionCompetenceRelations.link(competenceId, positionId);
+        return this.listPositions(competenceId);
+    }
+ 
+    async detachPosition(
+        competenceId: number,
+        positionId: number,
+    ): Promise<void> {
+        await this.getById(competenceId);
+        await this.positionCompetenceRelations.unlink(competenceId, positionId);
+    }
+ 
+    async listPositions(competenceId: number): Promise<number[]> {
+        await this.getById(competenceId);
+        const relations =
+            await this.positionCompetenceRelations.listByCompetence(
+                competenceId,
+            );
+        return relations.map((r) => r.positionId);
+    }
+ 
+    async attachQuestionTemplate(
+        competenceId: number,
+        questionTemplateId: number,
+    ): Promise<number[]> {
+        await this.getById(competenceId);
+        await this.ensureQuestionTemplateExists(questionTemplateId);
+ 
+        await this.competenceQuestionTemplateRelations.link(
+            competenceId,
+            questionTemplateId,
+        );
+        return this.listQuestionTemplates(competenceId);
+    }
+ 
+    async detachQuestionTemplate(
+        competenceId: number,
+        questionTemplateId: number,
+    ): Promise<void> {
+        await this.getById(competenceId);
+        await this.competenceQuestionTemplateRelations.unlink(
+            competenceId,
+            questionTemplateId,
+        );
+    }
+ 
+    async listQuestionTemplates(competenceId: number): Promise<number[]> {
+        await this.getById(competenceId);
+        const relations =
+            await this.competenceQuestionTemplateRelations.listByCompetence(
+                competenceId,
+            );
+        return relations.map((r) => r.questionTemplateId);
+    }
+ 
+    private async ensureQuestionTemplateExists(
+        questionTemplateId: number,
+    ): Promise<void> {
+        const found = await this.questionTemplates.findById(questionTemplateId);
+        if (!found) throw new NotFoundException('Question template not found');
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/services/index.html b/apps/api/test/integration/coverage/lcov-report/library/application/services/index.html new file mode 100644 index 00000000..c166f4ba --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/services/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for library/application/services + + + + + + + + + +
+
+

All files library/application/services

+
+ +
+ 98.08% + Statements + 154/157 +
+ + +
+ 69.53% + Branches + 89/128 +
+ + +
+ 100% + Functions + 38/38 +
+ + +
+ 97.9% + Lines + 140/143 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cluster.service.ts +
+
100%31/3170.58%24/34100%7/7100%28/28
competence.service.ts +
+
100%56/5668.42%26/38100%15/15100%50/50
question-template.service.ts +
+
95.71%67/7069.64%39/56100%16/1695.38%62/65
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/application/services/question-template.service.ts.html b/apps/api/test/integration/coverage/lcov-report/library/application/services/question-template.service.ts.html new file mode 100644 index 00000000..104e5513 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/application/services/question-template.service.ts.html @@ -0,0 +1,763 @@ + + + + + + Code coverage report for library/application/services/question-template.service.ts + + + + + + + + + +
+
+

All files / library/application/services question-template.service.ts

+
+ +
+ 95.71% + Statements + 67/70 +
+ + +
+ 69.64% + Branches + 39/56 +
+ + +
+ 100% + Functions + 16/16 +
+ + +
+ 95.38% + Lines + 62/65 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +22744x +  +  +  +  +  +44x +  +  +  +  +  +44x +  +  +  +44x +44x +44x +  +  +  +44x +  +  +  +44x +  +  +  +44x +  +  +44x +  +  +44x +  +44x +  +44x +44x +44x +  +44x +  +  +  +  +  +47x +  +46x +  +  +  +  +  +  +  +  +46x +  +45x +  +  +45x +1x +1x +  +  +  +  +  +45x +  +  +  +  +  +1x +  +  +  +68x +68x +1x +67x +  +  +  +  +  +  +2x +  +2x +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +2x +2x +  +  +2x +1x +  +  +  +  +  +  +  +1x +  +  +1x +  +  +1x +  +  +  +  +  +  +3x +3x +  +2x +  +  +  +2x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +3x +  +3x +  +  +3x +  +  +  +  +  +  +3x +3x +  +2x +  +  +  +2x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +3x +  +3x +  +  +3x +  +  +  +2x +  +  + 
import {
+    CreateQuestionTemplatePayload,
+    QuestionTemplateSearchQuery,
+    QuestionTemplateStatus,
+    UpdateQuestionTemplatePayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import {
+    QUESTION_REPOSITORY,
+    QuestionRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/question.repository.port';
+import { PositionService } from 'src/contexts/organisation/application/services/position.service';
+import { QuestionTemplateDomain } from '../../domain/question-template.domain';
+import {
+    COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY,
+    CompetenceQuestionTemplateRelationRepositoryPort,
+} from '../ports/competence-question-template-relation.repository.port';
+import {
+    POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY,
+    PositionQuestionTemplateRelationRepositoryPort,
+} from '../ports/position-question-template-relation.repository.port';
+import {
+    QUESTION_TEMPLATE_REPOSITORY,
+    QuestionTemplateRepositoryPort,
+} from '../ports/question-template.repository.port';
+import { CompetenceService } from './competence.service';
+ 
+@Injectable()
+export class QuestionTemplateService {
+    constructor(
+        @Inject(QUESTION_TEMPLATE_REPOSITORY)
+        private readonly questionTemplates: QuestionTemplateRepositoryPort,
+        @Inject(POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY)
+        private readonly positionQuestionTemplateRelations: PositionQuestionTemplateRelationRepositoryPort,
+        @Inject(COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY)
+        private readonly competenceQuestionTemplateRelations: CompetenceQuestionTemplateRelationRepositoryPort,
+        private readonly competences: CompetenceService,
+        private readonly positions: PositionService,
+        @Inject(QUESTION_REPOSITORY)
+        private readonly reviewQuestions: QuestionRepositoryPort,
+    ) {}
+ 
+    async create(
+        payload: CreateQuestionTemplatePayload,
+    ): Promise<QuestionTemplateDomain> {
+        await this.competences.getById(payload.competenceId);
+ 
+        const question = QuestionTemplateDomain.create({
+            competenceId: payload.competenceId,
+            title: payload.title,
+            answerType: payload.answerType,
+            isForSelfassessment: payload.isForSelfassessment ?? false,
+            status: payload.status ?? QuestionTemplateStatus.ACTIVE,
+            positionIds: payload.positionIds ?? [],
+        });
+ 
+        const created = await this.questionTemplates.create(question);
+ 
+        const positionIds = payload.positionIds
+            ? Array.from(new Set(payload.positionIds))
+            : [];
+        if (positionIds.length) {
+            await this.ensurePositionsExist(positionIds);
+            await this.positionQuestionTemplateRelations.replace(
+                created.id!,
+                positionIds,
+            );
+        }
+ 
+        return this.getById(created.id!);
+    }
+ 
+    async search(
+        query: QuestionTemplateSearchQuery,
+    ): Promise<QuestionTemplateDomain[]> {
+        return this.questionTemplates.search(query);
+    }
+ 
+    async getById(id: number): Promise<QuestionTemplateDomain> {
+        const question = await this.questionTemplates.findById(id);
+        if (!question)
+            throw new NotFoundException('Question template not found');
+        return question;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateQuestionTemplatePayload,
+    ): Promise<QuestionTemplateDomain> {
+        const current = await this.getById(id);
+ 
+        if (patch.competenceId && patch.competenceId !== current.competenceId) {
+            await this.competences.getById(patch.competenceId);
+        }
+ 
+        const payload: UpdateQuestionTemplatePayload = {
+            ...(patch.competenceId !== undefined
+                ? { competenceId: patch.competenceId }
+                : {}),
+            ...(patch.title !== undefined ? { title: patch.title } : {}),
+            ...(patch.answerType !== undefined
+                ? { answerType: patch.answerType }
+                : {}),
+            ...(patch.isForSelfassessment !== undefined
+                ? { isForSelfassessment: patch.isForSelfassessment }
+                : {}),
+            ...(patch.status !== undefined ? { status: patch.status } : {}),
+            ...(patch.positionIds !== undefined
+                ? { positionIds: patch.positionIds }
+                : {}),
+        };
+ 
+        await this.questionTemplates.updateById(id, payload);
+ 
+        Iif (patch.positionIds) {
+            const uniquePositions = Array.from(new Set(patch.positionIds));
+            await this.ensurePositionsExist(uniquePositions);
+            await this.positionQuestionTemplateRelations.replace(
+                id,
+                uniquePositions,
+            );
+        }
+ 
+        return this.getById(id);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        const relatedQuestions = await this.reviewQuestions.search({
+            questionTemplateId: id,
+        });
+        if (relatedQuestions.length > 0) {
+            throw new BadRequestException(
+                'Question template #' +
+                    id +
+                    ' cannot be deleted. It has ' +
+                    relatedQuestions.length +
+                    ' review questions assigned to it.',
+            );
+        }
+        await this.positionQuestionTemplateRelations.deleteAllForQuestionTemplate(
+            id,
+        );
+        await this.competenceQuestionTemplateRelations.deleteAllForQuestionTemplate(
+            id,
+        );
+        await this.questionTemplates.deleteById(id);
+    }
+ 
+    async attachPosition(
+        questionId: number,
+        positionId: number,
+    ): Promise<number[]> {
+        await this.getById(questionId);
+        await this.positions.getById(positionId);
+ 
+        await this.positionQuestionTemplateRelations.link(
+            questionId,
+            positionId,
+        );
+        return this.listPositions(questionId);
+    }
+ 
+    async detachPosition(
+        questionId: number,
+        positionId: number,
+    ): Promise<void> {
+        await this.getById(questionId);
+        await this.positionQuestionTemplateRelations.unlink(
+            questionId,
+            positionId,
+        );
+    }
+ 
+    async listPositions(questionId: number): Promise<number[]> {
+        await this.getById(questionId);
+        const relations =
+            await this.positionQuestionTemplateRelations.listByQuestion(
+                questionId,
+            );
+        return relations.map((r) => r.positionId);
+    }
+ 
+    async attachCompetence(
+        questionId: number,
+        competenceId: number,
+    ): Promise<number[]> {
+        await this.getById(questionId);
+        await this.competences.getById(competenceId);
+ 
+        await this.competenceQuestionTemplateRelations.link(
+            competenceId,
+            questionId,
+        );
+        return this.listCompetences(questionId);
+    }
+ 
+    async detachCompetence(
+        questionId: number,
+        competenceId: number,
+    ): Promise<void> {
+        await this.getById(questionId);
+        await this.competenceQuestionTemplateRelations.unlink(
+            competenceId,
+            questionId,
+        );
+    }
+ 
+    async listCompetences(questionId: number): Promise<number[]> {
+        await this.getById(questionId);
+        const relations =
+            await this.competenceQuestionTemplateRelations.listByQuestionTemplate(
+                questionId,
+            );
+        return relations.map((r) => r.competenceId);
+    }
+ 
+    private async ensurePositionsExist(positionIds: number[]): Promise<void> {
+        await Promise.all(positionIds.map((id) => this.positions.getById(id)));
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/cluster.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/cluster.mapper.ts.html new file mode 100644 index 00000000..6a5802f9 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/cluster.mapper.ts.html @@ -0,0 +1,172 @@ + + + + + + Code coverage report for library/infrastructure/mappers/cluster.mapper.ts + + + + + + + + + +
+
+

All files / library/infrastructure/mappers cluster.mapper.ts

+
+ +
+ 100% + Statements + 4/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 4/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30  +28x +  +28x +  +54x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +37x +  +  +  +  +  +  +  +  + 
import { Cluster, Prisma } from '@intra/database';
+import { ClusterDomain } from '../../domain/cluster.domain';
+ 
+export class ClusterMapper {
+    static toDomain(cluster: Cluster): ClusterDomain {
+        return ClusterDomain.create({
+            id: cluster.id,
+            competenceId: cluster.competenceId,
+            lowerBound: cluster.lowerBound?.toNumber(),
+            upperBound: cluster.upperBound?.toNumber(),
+            title: cluster.title,
+            description: cluster.description,
+            createdAt: cluster.createdAt,
+            updatedAt: cluster.updatedAt,
+        });
+    }
+ 
+    static toPrisma(
+        cluster: ClusterDomain,
+    ): Prisma.ClusterUncheckedCreateInput {
+        return {
+            competenceId: cluster.competenceId,
+            lowerBound: cluster.lowerBound,
+            upperBound: cluster.upperBound,
+            title: cluster.title,
+            description: cluster.description,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/competence-question-template-relation.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/competence-question-template-relation.mapper.ts.html new file mode 100644 index 00000000..506d47b5 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/competence-question-template-relation.mapper.ts.html @@ -0,0 +1,130 @@ + + + + + + Code coverage report for library/infrastructure/mappers/competence-question-template-relation.mapper.ts + + + + + + + + + +
+
+

All files / library/infrastructure/mappers competence-question-template-relation.mapper.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16  +9x +  +9x +  +  +  +23x +  +  +  +  +  +  +  + 
import { CompetenceQuestionTemplateRelation as PrismaCompetenceQuestionTemplateRelation } from '@intra/database';
+import { CompetenceQuestionTemplateRelationDomain } from '../../domain/competence-question-template-relation.domain';
+ 
+export class CompetenceQuestionTemplateRelationMapper {
+    static toDomain(
+        relation: PrismaCompetenceQuestionTemplateRelation,
+    ): CompetenceQuestionTemplateRelationDomain {
+        return CompetenceQuestionTemplateRelationDomain.create({
+            id: relation.id,
+            competenceId: relation.competenceId,
+            questionTemplateId: relation.questionTemplateId,
+            createdAt: relation.createdAt,
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/competence.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/competence.mapper.ts.html new file mode 100644 index 00000000..23013f14 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/competence.mapper.ts.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for library/infrastructure/mappers/competence.mapper.ts + + + + + + + + + +
+
+

All files / library/infrastructure/mappers competence.mapper.ts

+
+ +
+ 100% + Statements + 4/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 4/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26  +28x +  +28x +  +218x +  +  +  +  +  +  +  +  +  +  +  +  +146x +  +  +  +  +  +  + 
import { Competence, Prisma } from '@intra/database';
+import { CompetenceDomain } from '../../domain/competence.domain';
+ 
+export class CompetenceMapper {
+    static toDomain(competence: Competence): CompetenceDomain {
+        return CompetenceDomain.create({
+            id: competence.id,
+            code: competence.code,
+            title: competence.title,
+            description: competence.description,
+            createdAt: competence.createdAt,
+            updatedAt: competence.updatedAt,
+        });
+    }
+ 
+    static toPrisma(
+        competence: CompetenceDomain,
+    ): Prisma.CompetenceUncheckedCreateInput {
+        return {
+            code: competence.code,
+            title: competence.title,
+            description: competence.description,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/index.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/index.html new file mode 100644 index 00000000..06e11e27 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/index.html @@ -0,0 +1,191 @@ + + + + + + Code coverage report for library/infrastructure/mappers + + + + + + + + + +
+
+

All files library/infrastructure/mappers

+
+ +
+ 92.3% + Statements + 24/26 +
+ + +
+ 50% + Branches + 2/4 +
+ + +
+ 85.71% + Functions + 12/14 +
+ + +
+ 92.3% + Lines + 24/26 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cluster.mapper.ts +
+
100%4/4100%0/0100%2/2100%4/4
competence-question-template-relation.mapper.ts +
+
100%3/3100%0/0100%1/1100%3/3
competence.mapper.ts +
+
100%4/4100%0/0100%2/2100%4/4
position-competence-relation.mapper.ts +
+
100%3/3100%0/0100%1/1100%3/3
position-question-template-relation.mapper.ts +
+
100%3/3100%0/0100%1/1100%3/3
question-template.mapper.ts +
+
77.77%7/950%2/471.42%5/777.77%7/9
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/position-competence-relation.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/position-competence-relation.mapper.ts.html new file mode 100644 index 00000000..fd8fef3a --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/position-competence-relation.mapper.ts.html @@ -0,0 +1,130 @@ + + + + + + Code coverage report for library/infrastructure/mappers/position-competence-relation.mapper.ts + + + + + + + + + +
+
+

All files / library/infrastructure/mappers position-competence-relation.mapper.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16  +15x +  +15x +  +  +  +31x +  +  +  +  +  +  +  + 
import { PositionCompetenceRelation as PrismaPositionCompetenceRelation } from '@intra/database';
+import { PositionCompetenceRelationDomain } from '../../domain/position-competence-relation.domain';
+ 
+export class PositionCompetenceRelationMapper {
+    static toDomain(
+        relation: PrismaPositionCompetenceRelation,
+    ): PositionCompetenceRelationDomain {
+        return PositionCompetenceRelationDomain.create({
+            id: relation.id,
+            positionId: relation.positionId,
+            competenceId: relation.competenceId,
+            createdAt: relation.createdAt,
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/position-question-template-relation.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/position-question-template-relation.mapper.ts.html new file mode 100644 index 00000000..418e4306 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/position-question-template-relation.mapper.ts.html @@ -0,0 +1,130 @@ + + + + + + Code coverage report for library/infrastructure/mappers/position-question-template-relation.mapper.ts + + + + + + + + + +
+
+

All files / library/infrastructure/mappers position-question-template-relation.mapper.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16  +9x +  +9x +  +  +  +22x +  +  +  +  +  +  +  + 
import { PositionQuestionTemplateRelation as PrismaPositionQuestionTemplateRelation } from '@intra/database';
+import { PositionQuestionTemplateRelationDomain } from '../../domain/position-question-template-relation.domain';
+ 
+export class PositionQuestionTemplateRelationMapper {
+    static toDomain(
+        relation: PrismaPositionQuestionTemplateRelation,
+    ): PositionQuestionTemplateRelationDomain {
+        return PositionQuestionTemplateRelationDomain.create({
+            id: relation.id,
+            questionTemplateId: relation.questionTemplateId,
+            positionId: relation.positionId,
+            createdAt: relation.createdAt,
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/question-template.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/question-template.mapper.ts.html new file mode 100644 index 00000000..33a15d44 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/mappers/question-template.mapper.ts.html @@ -0,0 +1,280 @@ + + + + + + Code coverage report for library/infrastructure/mappers/question-template.mapper.ts + + + + + + + + + +
+
+

All files / library/infrastructure/mappers question-template.mapper.ts

+
+ +
+ 77.77% + Statements + 7/9 +
+ + +
+ 50% + Branches + 2/4 +
+ + +
+ 71.42% + Functions + 5/7 +
+ + +
+ 77.77% + Lines + 7/9 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66  +  +  +  +  +  +  +9x +  +  +  +  +  +9x +  +88x +  +  +  +  +  +  +  +  +  +  +  +9x +  +  +  +  +  +  +44x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +88x +  +  +  +  +  +88x +  +  + 
import {
+    Prisma,
+    AnswerType as PrismaAnswerType,
+    QuestionTemplate as PrismaQuestionTemplate,
+    QuestionTemplateStatus as PrismaQuestionTemplateStatus,
+} from '@intra/database';
+import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel';
+import { QuestionTemplateDomain } from '../../domain/question-template.domain';
+ 
+type QuestionWithPositions = PrismaQuestionTemplate & {
+    positionRelations?: { positionId: number }[];
+};
+ 
+export class QuestionTemplateMapper {
+    static toDomain(question: QuestionWithPositions): QuestionTemplateDomain {
+        return QuestionTemplateDomain.create({
+            id: question.id,
+            title: question.title,
+            answerType: QuestionTemplateMapper.fromPrismaAnswerType(
+                question.answerType,
+            ),
+            competenceId: question.competenceId,
+            isForSelfassessment: question.isForSelfassessment ?? false,
+            status: QuestionTemplateMapper.fromPrismaStatus(question.status),
+            createdAt: question.createdAt,
+            updatedAt: question.updatedAt,
+            positionIds:
+                question.positionRelations?.map((q) => q.positionId) ?? [],
+        });
+    }
+ 
+    static toPrisma(
+        question: QuestionTemplateDomain,
+    ): Prisma.QuestionTemplateUncheckedCreateInput {
+        return {
+            title: question.title,
+            answerType: question.answerType,
+            competenceId: question.competenceId,
+            isForSelfassessment: question.isForSelfassessment,
+            status: question.status,
+        };
+    }
+ 
+    static toPrismaAnswerType(domainType: AnswerType): PrismaAnswerType {
+        return domainType.toString().toUpperCase() as PrismaAnswerType;
+    }
+ 
+    static toPrismaStatus(
+        domainStatus: QuestionTemplateStatus,
+    ): PrismaQuestionTemplateStatus {
+        return domainStatus
+            .toString()
+            .toUpperCase() as PrismaQuestionTemplateStatus;
+    }
+ 
+    static fromPrismaAnswerType(prismaType: PrismaAnswerType): AnswerType {
+        return prismaType.toString().toUpperCase() as AnswerType;
+    }
+ 
+    static fromPrismaStatus(
+        prismaStatus: PrismaQuestionTemplateStatus,
+    ): QuestionTemplateStatus {
+        return prismaStatus.toString().toUpperCase() as QuestionTemplateStatus;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/cluster.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/cluster.repository.ts.html new file mode 100644 index 00000000..bb01a6ba --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/cluster.repository.ts.html @@ -0,0 +1,391 @@ + + + + + + Code coverage report for library/infrastructure/prisma-repositories/cluster.repository.ts + + + + + + + + + +
+
+

All files / library/infrastructure/prisma-repositories cluster.repository.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 77.27% + Branches + 17/22 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 22/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103  +44x +  +  +  +  +  +44x +44x +  +  +44x +  +  +44x +44x +  +  +56x +  +  +  +56x +  +  +  +9x +9x +  +  +  +12x +12x +  +12x +12x +  +  +  +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +12x +12x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +12x +12x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    ClusterSearchQuery,
+    ClusterSortField,
+    SortDirection,
+    UpdateClusterPayload,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { ClusterRepositoryPort } from '../../application/ports/cluster.repository.port';
+import { ClusterDomain } from '../../domain/cluster.domain';
+import { ClusterMapper } from '../mappers/cluster.mapper';
+ 
+@Injectable()
+export class ClusterRepository implements ClusterRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(cluster: ClusterDomain): Promise<ClusterDomain> {
+        const created = await this.prisma.cluster.create({
+            data: ClusterMapper.toPrisma(cluster),
+        });
+ 
+        return ClusterMapper.toDomain(created);
+    }
+ 
+    async findById(id: number): Promise<ClusterDomain | null> {
+        const cluster = await this.prisma.cluster.findUnique({ where: { id } });
+        return cluster ? ClusterMapper.toDomain(cluster) : null;
+    }
+ 
+    async search(query: ClusterSearchQuery): Promise<ClusterDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+ 
+        const items = await this.prisma.cluster.findMany({ where, orderBy });
+        return items.map(ClusterMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateClusterPayload,
+    ): Promise<ClusterDomain> {
+        const updated = await this.prisma.cluster.update({
+            where: { id },
+            data: patch,
+        });
+ 
+        return ClusterMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.cluster.delete({ where: { id } });
+    }
+ 
+    private buildWhere(query: ClusterSearchQuery): Prisma.ClusterWhereInput {
+        const {
+            competenceId,
+            lowerBound,
+            upperBound,
+            title,
+            description,
+            search,
+        } = query;
+        return {
+            ...(competenceId ? { competenceId } : {}),
+            ...(lowerBound ? { lowerBound } : {}),
+            ...(upperBound ? { upperBound } : {}),
+            ...(title
+                ? { title: { contains: title, mode: 'insensitive' } }
+                : {}),
+            ...(description
+                ? {
+                      description: {
+                          contains: description,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(search
+                ? {
+                      OR: [
+                          { title: { contains: search, mode: 'insensitive' } },
+                          {
+                              description: {
+                                  contains: search,
+                                  mode: 'insensitive',
+                              },
+                          },
+                      ],
+                  }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: ClusterSearchQuery,
+    ): Prisma.ClusterOrderByWithRelationInput[] {
+        const field = query.sortBy ?? ClusterSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/competence-question-template-relation.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/competence-question-template-relation.repository.ts.html new file mode 100644 index 00000000..b7440ee2 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/competence-question-template-relation.repository.ts.html @@ -0,0 +1,454 @@ + + + + + + Code coverage report for library/infrastructure/prisma-repositories/competence-question-template-relation.repository.ts + + + + + + + + + +
+
+

All files / library/infrastructure/prisma-repositories competence-question-template-relation.repository.ts

+
+ +
+ 70.96% + Statements + 22/31 +
+ + +
+ 50% + Branches + 7/14 +
+ + +
+ 70% + Functions + 7/10 +
+ + +
+ 68.96% + Lines + 20/29 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +12444x +44x +44x +  +  +44x +  +  +44x +44x +  +  +  +  +  +14x +  +14x +  +  +13x +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +8x +  +  +  +8x +  +  +  +  +  +  +5x +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  + 
import { Prisma } from '@intra/database';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { CompetenceQuestionTemplateRelationRepositoryPort } from '../../application/ports/competence-question-template-relation.repository.port';
+import { CompetenceQuestionTemplateRelationDomain } from '../../domain/competence-question-template-relation.domain';
+import { CompetenceQuestionTemplateRelationMapper } from '../mappers/competence-question-template-relation.mapper';
+ 
+@Injectable()
+export class CompetenceQuestionTemplateRelationRepository implements CompetenceQuestionTemplateRelationRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async link(
+        competenceId: number,
+        questionTemplateId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain> {
+        try {
+            const relation =
+                await this.prisma.competenceQuestionTemplateRelation.create({
+                    data: { competenceId, questionTemplateId },
+                });
+            return CompetenceQuestionTemplateRelationMapper.toDomain(relation);
+        } catch (error) {
+            Eif (
+                error instanceof Prisma.PrismaClientKnownRequestError &&
+                error.code === 'P2002'
+            ) {
+                const existing =
+                    await this.prisma.competenceQuestionTemplateRelation.findUnique(
+                        {
+                            where: {
+                                competenceId_questionTemplateId: {
+                                    competenceId,
+                                    questionTemplateId,
+                                },
+                            },
+                        },
+                    );
+                Eif (existing)
+                    return CompetenceQuestionTemplateRelationMapper.toDomain(
+                        existing,
+                    );
+            }
+            throw error;
+        }
+    }
+ 
+    async unlink(
+        competenceId: number,
+        questionTemplateId: number,
+    ): Promise<void> {
+        await this.prisma.competenceQuestionTemplateRelation.deleteMany({
+            where: { competenceId, questionTemplateId },
+        });
+    }
+ 
+    async listByCompetence(
+        competenceId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]> {
+        const relations =
+            await this.prisma.competenceQuestionTemplateRelation.findMany({
+                where: { competenceId },
+                orderBy: { createdAt: 'desc' },
+            });
+        return relations.map(CompetenceQuestionTemplateRelationMapper.toDomain);
+    }
+ 
+    async listByQuestionTemplate(
+        questionTemplateId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]> {
+        const relations =
+            await this.prisma.competenceQuestionTemplateRelation.findMany({
+                where: { questionTemplateId },
+                orderBy: { createdAt: 'desc' },
+            });
+        return relations.map(CompetenceQuestionTemplateRelationMapper.toDomain);
+    }
+ 
+    async replaceForCompetence(
+        competenceId: number,
+        questionTemplateIds: number[],
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]> {
+        const unique = Array.from(new Set(questionTemplateIds));
+ 
+        await this.prisma.$transaction(async (tx) => {
+            const deleteFilter =
+                unique.length > 0
+                    ? {
+                          competenceId,
+                          NOT: { questionTemplateId: { in: unique } },
+                      }
+                    : { competenceId };
+            await tx.competenceQuestionTemplateRelation.deleteMany({
+                where: deleteFilter,
+            });
+ 
+            if (unique.length) {
+                await tx.competenceQuestionTemplateRelation.createMany({
+                    data: unique.map((questionTemplateId) => ({
+                        competenceId,
+                        questionTemplateId,
+                    })),
+                    skipDuplicates: true,
+                });
+            }
+        });
+ 
+        return this.listByCompetence(competenceId);
+    }
+ 
+    async deleteAllForQuestionTemplate(
+        questionTemplateId: number,
+    ): Promise<void> {
+        await this.prisma.competenceQuestionTemplateRelation.deleteMany({
+            where: { questionTemplateId },
+        });
+    }
+ 
+    async deleteAllForCompetence(competenceId: number): Promise<void> {
+        await this.prisma.competenceQuestionTemplateRelation.deleteMany({
+            where: { competenceId },
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/competence.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/competence.repository.ts.html new file mode 100644 index 00000000..ab9d2478 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/competence.repository.ts.html @@ -0,0 +1,379 @@ + + + + + + Code coverage report for library/infrastructure/prisma-repositories/competence.repository.ts + + + + + + + + + +
+
+

All files / library/infrastructure/prisma-repositories competence.repository.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 88.88% + Branches + 16/18 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 22/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99  +44x +  +  +  +  +  +44x +44x +  +  +44x +  +  +44x +44x +  +  +217x +  +  +  +217x +  +  +  +118x +  +  +118x +  +  +  +6x +6x +  +6x +6x +  +  +  +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +6x +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x +6x +6x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    CompetenceSearchQuery,
+    CompetenceSortField,
+    SortDirection,
+    UpdateCompetencePayload,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { CompetenceRepositoryPort } from '../../application/ports/competence.repository.port';
+import { CompetenceDomain } from '../../domain/competence.domain';
+import { CompetenceMapper } from '../mappers/competence.mapper';
+ 
+@Injectable()
+export class CompetenceRepository implements CompetenceRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(competence: CompetenceDomain): Promise<CompetenceDomain> {
+        const created = await this.prisma.competence.create({
+            data: CompetenceMapper.toPrisma(competence),
+        });
+ 
+        return CompetenceMapper.toDomain(created);
+    }
+ 
+    async findById(id: number): Promise<CompetenceDomain | null> {
+        const competence = await this.prisma.competence.findUnique({
+            where: { id },
+        });
+        return competence ? CompetenceMapper.toDomain(competence) : null;
+    }
+ 
+    async search(query: CompetenceSearchQuery): Promise<CompetenceDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+ 
+        const items = await this.prisma.competence.findMany({ where, orderBy });
+        return items.map(CompetenceMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateCompetencePayload,
+    ): Promise<CompetenceDomain> {
+        const updated = await this.prisma.competence.update({
+            where: { id },
+            data: patch,
+        });
+ 
+        return CompetenceMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.competence.delete({ where: { id } });
+    }
+ 
+    private buildWhere(
+        query: CompetenceSearchQuery,
+    ): Prisma.CompetenceWhereInput {
+        const { search, code, title, description } = query;
+        return {
+            ...(code ? { code: { contains: code, mode: 'insensitive' } } : {}),
+            ...(title
+                ? { title: { contains: title, mode: 'insensitive' } }
+                : {}),
+            ...(description
+                ? {
+                      description: {
+                          contains: description,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(search
+                ? {
+                      OR: [
+                          { title: { contains: search, mode: 'insensitive' } },
+                          { code: { contains: search, mode: 'insensitive' } },
+                          {
+                              description: {
+                                  contains: search,
+                                  mode: 'insensitive',
+                              },
+                          },
+                      ],
+                  }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: CompetenceSearchQuery,
+    ): Prisma.CompetenceOrderByWithRelationInput[] {
+        const field = query.sortBy ?? CompetenceSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/index.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/index.html new file mode 100644 index 00000000..dcb9e28d --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/index.html @@ -0,0 +1,191 @@ + + + + + + Code coverage report for library/infrastructure/prisma-repositories + + + + + + + + + +
+
+

All files library/infrastructure/prisma-repositories

+
+ +
+ 93.29% + Statements + 153/164 +
+ + +
+ 75.47% + Branches + 80/106 +
+ + +
+ 94.23% + Functions + 49/52 +
+ + +
+ 92.76% + Lines + 141/152 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cluster.repository.ts +
+
100%24/2477.27%17/22100%8/8100%22/22
competence-question-template-relation.repository.ts +
+
70.96%22/3150%7/1470%7/1068.96%20/29
competence.repository.ts +
+
100%24/2488.88%16/18100%8/8100%22/22
position-competence-relation.repository.ts +
+
96.66%29/3078.57%11/14100%9/996.42%27/28
position-question-template-relation.repository.ts +
+
96.42%27/2878.57%11/14100%8/896.15%25/26
question-template.repository.ts +
+
100%27/2775%18/24100%9/9100%25/25
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/position-competence-relation.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/position-competence-relation.repository.ts.html new file mode 100644 index 00000000..aae22e43 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/position-competence-relation.repository.ts.html @@ -0,0 +1,406 @@ + + + + + + Code coverage report for library/infrastructure/prisma-repositories/position-competence-relation.repository.ts + + + + + + + + + +
+
+

All files / library/infrastructure/prisma-repositories position-competence-relation.repository.ts

+
+ +
+ 96.66% + Statements + 29/30 +
+ + +
+ 78.57% + Branches + 11/14 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 96.42% + Lines + 27/28 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +10850x +50x +50x +  +  +50x +  +  +50x +50x +  +  +  +  +  +17x +  +17x +  +  +15x +  +2x +  +  +  +  +2x +  +  +  +  +  +  +  +2x +2x +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +12x +  +  +  +  +  +12x +  +  +  +  +  +5x +  +  +  +  +  +5x +  +  +  +  +  +  +2x +  +2x +  +2x +  +  +2x +  +  +  +2x +1x +2x +  +  +  +  +  +  +  +  +2x +  +  +  +2x +  +  +  +  + 
import { Prisma } from '@intra/database';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { PositionCompetenceRelationRepositoryPort } from '../../application/ports/position-competence-relation.repository.port';
+import { PositionCompetenceRelationDomain } from '../../domain/position-competence-relation.domain';
+import { PositionCompetenceRelationMapper } from '../mappers/position-competence-relation.mapper';
+ 
+@Injectable()
+export class PositionCompetenceRelationRepository implements PositionCompetenceRelationRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async link(
+        competenceId: number,
+        positionId: number,
+    ): Promise<PositionCompetenceRelationDomain> {
+        try {
+            const relation =
+                await this.prisma.positionCompetenceRelation.create({
+                    data: { competenceId, positionId },
+                });
+            return PositionCompetenceRelationMapper.toDomain(relation);
+        } catch (error) {
+            Eif (
+                error instanceof Prisma.PrismaClientKnownRequestError &&
+                error.code === 'P2002'
+            ) {
+                const existing =
+                    await this.prisma.positionCompetenceRelation.findUnique({
+                        where: {
+                            positionId_competenceId: {
+                                positionId,
+                                competenceId,
+                            },
+                        },
+                    });
+                Eif (existing)
+                    return PositionCompetenceRelationMapper.toDomain(existing);
+            }
+            throw error;
+        }
+    }
+ 
+    async unlink(competenceId: number, positionId: number): Promise<void> {
+        await this.prisma.positionCompetenceRelation.deleteMany({
+            where: { competenceId, positionId },
+        });
+    }
+ 
+    async listByCompetence(
+        competenceId: number,
+    ): Promise<PositionCompetenceRelationDomain[]> {
+        const relations = await this.prisma.positionCompetenceRelation.findMany(
+            {
+                where: { competenceId },
+                orderBy: { createdAt: 'desc' },
+            },
+        );
+        return relations.map(PositionCompetenceRelationMapper.toDomain);
+    }
+ 
+    async listByPosition(
+        positionId: number,
+    ): Promise<PositionCompetenceRelationDomain[]> {
+        const relations = await this.prisma.positionCompetenceRelation.findMany(
+            {
+                where: { positionId },
+                orderBy: { createdAt: 'desc' },
+            },
+        );
+        return relations.map(PositionCompetenceRelationMapper.toDomain);
+    }
+ 
+    async replaceForCompetence(
+        competenceId: number,
+        positionIds: number[],
+    ): Promise<PositionCompetenceRelationDomain[]> {
+        const unique = Array.from(new Set(positionIds));
+ 
+        await this.prisma.$transaction(async (tx) => {
+            const deleteFilter =
+                unique.length > 0
+                    ? { competenceId, NOT: { positionId: { in: unique } } }
+                    : { competenceId };
+            await tx.positionCompetenceRelation.deleteMany({
+                where: deleteFilter,
+            });
+ 
+            if (unique.length) {
+                await tx.positionCompetenceRelation.createMany({
+                    data: unique.map((positionId) => ({
+                        competenceId,
+                        positionId,
+                    })),
+                    skipDuplicates: true,
+                });
+            }
+        });
+ 
+        return this.listByCompetence(competenceId);
+    }
+ 
+    async deleteAllForCompetence(competenceId: number): Promise<void> {
+        await this.prisma.positionCompetenceRelation.deleteMany({
+            where: { competenceId },
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/position-question-template-relation.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/position-question-template-relation.repository.ts.html new file mode 100644 index 00000000..7c79ef35 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/position-question-template-relation.repository.ts.html @@ -0,0 +1,397 @@ + + + + + + Code coverage report for library/infrastructure/prisma-repositories/position-question-template-relation.repository.ts + + + + + + + + + +
+
+

All files / library/infrastructure/prisma-repositories position-question-template-relation.repository.ts

+
+ +
+ 96.42% + Statements + 27/28 +
+ + +
+ 78.57% + Branches + 11/14 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 96.15% + Lines + 25/26 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +10544x +44x +44x +  +  +44x +  +  +44x +44x +  +  +  +  +  +13x +  +13x +  +  +12x +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +9x +  +  +  +  +9x +  +  +  +  +  +  +4x +  +4x +  +4x +  +  +  +  +  +4x +  +  +  +4x +3x +5x +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +2x +  +  +  +  + 
import { Prisma } from '@intra/database';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { PositionQuestionTemplateRelationRepositoryPort } from '../../application/ports/position-question-template-relation.repository.port';
+import { PositionQuestionTemplateRelationDomain } from '../../domain/position-question-template-relation.domain';
+import { PositionQuestionTemplateRelationMapper } from '../mappers/position-question-template-relation.mapper';
+ 
+@Injectable()
+export class PositionQuestionTemplateRelationRepository implements PositionQuestionTemplateRelationRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async link(
+        questionId: number,
+        positionId: number,
+    ): Promise<PositionQuestionTemplateRelationDomain> {
+        try {
+            const relation =
+                await this.prisma.positionQuestionTemplateRelation.create({
+                    data: { questionTemplateId: questionId, positionId },
+                });
+            return PositionQuestionTemplateRelationMapper.toDomain(relation);
+        } catch (error) {
+            Eif (
+                error instanceof Prisma.PrismaClientKnownRequestError &&
+                error.code === 'P2002'
+            ) {
+                const existing =
+                    await this.prisma.positionQuestionTemplateRelation.findUnique(
+                        {
+                            where: {
+                                questionTemplateId_positionId: {
+                                    questionTemplateId: questionId,
+                                    positionId,
+                                },
+                            },
+                        },
+                    );
+                Eif (existing)
+                    return PositionQuestionTemplateRelationMapper.toDomain(
+                        existing,
+                    );
+            }
+            throw error;
+        }
+    }
+ 
+    async unlink(questionId: number, positionId: number): Promise<void> {
+        await this.prisma.positionQuestionTemplateRelation.deleteMany({
+            where: { questionTemplateId: questionId, positionId },
+        });
+    }
+ 
+    async listByQuestion(
+        questionId: number,
+    ): Promise<PositionQuestionTemplateRelationDomain[]> {
+        const relations =
+            await this.prisma.positionQuestionTemplateRelation.findMany({
+                where: { questionTemplateId: questionId },
+                orderBy: { createdAt: 'desc' },
+            });
+ 
+        return relations.map(PositionQuestionTemplateRelationMapper.toDomain);
+    }
+ 
+    async replace(
+        questionId: number,
+        positionIds: number[],
+    ): Promise<PositionQuestionTemplateRelationDomain[]> {
+        const unique = Array.from(new Set(positionIds));
+ 
+        await this.prisma.$transaction(async (tx) => {
+            const deleteFilter =
+                unique.length > 0
+                    ? {
+                          questionTemplateId: questionId,
+                          NOT: { positionId: { in: unique } },
+                      }
+                    : { questionTemplateId: questionId };
+            await tx.positionQuestionTemplateRelation.deleteMany({
+                where: deleteFilter,
+            });
+ 
+            if (unique.length) {
+                await tx.positionQuestionTemplateRelation.createMany({
+                    data: unique.map((positionId) => ({
+                        questionTemplateId: questionId,
+                        positionId,
+                    })),
+                    skipDuplicates: true,
+                });
+            }
+        });
+ 
+        return this.listByQuestion(questionId);
+    }
+ 
+    async deleteAllForQuestionTemplate(
+        questionTemplateId: number,
+    ): Promise<void> {
+        await this.prisma.positionQuestionTemplateRelation.deleteMany({
+            where: { questionTemplateId: questionTemplateId },
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/question-template.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/question-template.repository.ts.html new file mode 100644 index 00000000..762c0b01 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/library/infrastructure/prisma-repositories/question-template.repository.ts.html @@ -0,0 +1,457 @@ + + + + + + Code coverage report for library/infrastructure/prisma-repositories/question-template.repository.ts + + + + + + + + + +
+
+

All files / library/infrastructure/prisma-repositories question-template.repository.ts

+
+ +
+ 100% + Statements + 27/27 +
+ + +
+ 75% + Branches + 18/24 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 100% + Lines + 25/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125  +44x +  +  +  +  +  +44x +44x +  +  +44x +  +  +44x +44x +  +  +  +  +75x +75x +6x +  +  +  +75x +  +  +  +  +74x +  +  +  +73x +  +  +  +  +73x +  +  +  +  +  +5x +5x +  +5x +  +  +  +  +  +5x +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +5x +5x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    QuestionTemplateSearchQuery,
+    QuestionTemplateSortField,
+    SortDirection,
+    UpdateQuestionTemplatePayload,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { QuestionTemplateRepositoryPort } from '../../application/ports/question-template.repository.port';
+import { QuestionTemplateDomain } from '../../domain/question-template.domain';
+import { QuestionTemplateMapper } from '../mappers/question-template.mapper';
+ 
+@Injectable()
+export class QuestionTemplateRepository implements QuestionTemplateRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(
+        question: QuestionTemplateDomain,
+    ): Promise<QuestionTemplateDomain> {
+        const prismaQuestion = QuestionTemplateMapper.toPrisma(question);
+        prismaQuestion.positionRelations = {
+            create: question.positionIds.map((positionId) => ({
+                positionId,
+            })),
+        };
+        const created = await this.prisma.questionTemplate.create({
+            data: prismaQuestion,
+            include: { positionRelations: { select: { positionId: true } } },
+        });
+ 
+        return QuestionTemplateMapper.toDomain(created);
+    }
+ 
+    async findById(id: number): Promise<QuestionTemplateDomain | null> {
+        const question = await this.prisma.questionTemplate.findUnique({
+            where: { id },
+            include: { positionRelations: { select: { positionId: true } } },
+        });
+ 
+        return question ? QuestionTemplateMapper.toDomain(question) : null;
+    }
+ 
+    async search(
+        query: QuestionTemplateSearchQuery,
+    ): Promise<QuestionTemplateDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+ 
+        const items = await this.prisma.questionTemplate.findMany({
+            where,
+            orderBy,
+            include: { positionRelations: { select: { positionId: true } } },
+        });
+ 
+        return items.map(QuestionTemplateMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateQuestionTemplatePayload,
+    ): Promise<QuestionTemplateDomain> {
+        const updated = await this.prisma.questionTemplate.update({
+            where: { id },
+            data: {
+                ...patch,
+            },
+            include: { positionRelations: { select: { positionId: true } } },
+        });
+ 
+        return QuestionTemplateMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.questionTemplate.delete({ where: { id } });
+    }
+ 
+    private buildWhere(
+        query: QuestionTemplateSearchQuery,
+    ): Prisma.QuestionTemplateWhereInput {
+        const {
+            competenceId,
+            positionIds,
+            status: status,
+            answerType,
+            isForSelfassessment,
+            title,
+        } = query;
+ 
+        return {
+            ...(title
+                ? { title: { contains: title, mode: 'insensitive' } }
+                : {}),
+            ...(competenceId ? { competenceId } : {}),
+            ...(status ? { status } : {}),
+            ...(answerType ? { answerType } : {}),
+            ...(isForSelfassessment !== undefined
+                ? isForSelfassessment === true
+                    ? { isForSelfassessment: true }
+                    : {
+                          OR: [
+                              { isForSelfassessment: false },
+                              { isForSelfassessment: null },
+                          ],
+                      }
+                : {}),
+            ...(positionIds
+                ? {
+                      positionRelations: {
+                          some: { positionId: { in: positionIds } },
+                      },
+                  }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: QuestionTemplateSearchQuery,
+    ): Prisma.QuestionTemplateOrderByWithRelationInput[] {
+        const field = query.sortBy ?? QuestionTemplateSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/cycle-stage-notification.listener.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/cycle-stage-notification.listener.ts.html new file mode 100644 index 00000000..3bddb455 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/cycle-stage-notification.listener.ts.html @@ -0,0 +1,214 @@ + + + + + + Code coverage report for notifications/application/listeners/cycle-stage-notification.listener.ts + + + + + + + + + +
+
+

All files / notifications/application/listeners cycle-stage-notification.listener.ts

+
+ +
+ 90% + Statements + 18/20 +
+ + +
+ 61.11% + Branches + 11/18 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 88.88% + Lines + 16/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +443x +3x +3x +3x +3x +  +  +3x +2x +  +  +2x +  +  +  +3x +3x +  +3x +3x +  +  +2x +  +  +2x +  +  +2x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+import { Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { CycleStageChangedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-changed.event';
+import { ReviewEmailNotificationService } from '../services/review-email-notification.service';
+ 
+@Injectable()
+export class CycleStageNotificationListener {
+    private readonly logger = new Logger(CycleStageNotificationListener.name);
+ 
+    constructor(
+        private readonly notifications: ReviewEmailNotificationService,
+    ) {}
+ 
+    @OnEvent('cycle.stage.changed', { async: true })
+    async handle(event: CycleStageChangedEvent): Promise<void> {
+        const { cycleId, toStage } = event;
+ 
+        try {
+            switch (toStage) {
+                case CycleStage.PUBLISHED: {
+                    const sent =
+                        await this.notifications.notifyCycleStrategicReportReady(
+                            cycleId,
+                        );
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for cycle=${cycleId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                default:
+                    return;
+            }
+        } catch (error) {
+            const message =
+                error instanceof Error ? error.message : String(error);
+            this.logger.error(
+                `Failed to handle cycle.stage.changed for cycle=${cycleId} stage=${toStage}: ${message}`,
+                error instanceof Error ? error.stack : undefined,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/index.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/index.html new file mode 100644 index 00000000..c1b5da70 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for notifications/application/listeners + + + + + + + + + +
+
+

All files notifications/application/listeners

+
+ +
+ 90.76% + Statements + 59/65 +
+ + +
+ 61.81% + Branches + 34/55 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 89.83% + Lines + 53/59 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage-notification.listener.ts +
+
90%18/2061.11%11/18100%2/288.88%16/18
review-stage-notification.listener.ts +
+
93.1%27/2966.66%14/21100%2/292.59%25/27
user-created-notification.listener.ts +
+
87.5%14/1656.25%9/16100%2/285.71%12/14
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/review-stage-notification.listener.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/review-stage-notification.listener.ts.html new file mode 100644 index 00000000..c6ac7f70 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/review-stage-notification.listener.ts.html @@ -0,0 +1,286 @@ + + + + + + Code coverage report for notifications/application/listeners/review-stage-notification.listener.ts + + + + + + + + + +
+
+

All files / notifications/application/listeners review-stage-notification.listener.ts

+
+ +
+ 93.1% + Statements + 27/29 +
+ + +
+ 66.66% + Branches + 14/21 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 92.59% + Lines + 25/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +683x +3x +3x +3x +3x +  +  +3x +2x +  +  +2x +  +  +  +3x +5x +  +5x +5x +  +  +1x +  +  +1x +  +  +1x +  +  +  +1x +1x +  +  +1x +  +  +  +1x +1x +  +  +1x +  +  +  +1x +1x +  +  +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+import { Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { ReviewStageChangedEvent } from 'src/contexts/feedback360/application/events/review-stage-changed.event';
+import { ReviewEmailNotificationService } from '../services/review-email-notification.service';
+ 
+@Injectable()
+export class ReviewStageNotificationListener {
+    private readonly logger = new Logger(ReviewStageNotificationListener.name);
+ 
+    constructor(
+        private readonly notifications: ReviewEmailNotificationService,
+    ) {}
+ 
+    @OnEvent('review.stage.changed', { async: true })
+    async handle(event: ReviewStageChangedEvent): Promise<void> {
+        const { reviewId, toStage } = event;
+ 
+        try {
+            switch (toStage) {
+                case ReviewStage.SELF_ASSESSMENT: {
+                    const sent =
+                        await this.notifications.notifyRateeSelfAssessment(
+                            reviewId,
+                        );
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for review=${reviewId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                case ReviewStage.IN_PROGRESS: {
+                    const sent =
+                        await this.notifications.notifyRespondents(reviewId);
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for review=${reviewId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                case ReviewStage.PROCESSING_BY_HR: {
+                    const sent =
+                        await this.notifications.notifyHrReportReady(reviewId);
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for review=${reviewId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                case ReviewStage.PUBLISHED: {
+                    const sent =
+                        await this.notifications.notifyReviewers(reviewId);
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for review=${reviewId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                default:
+                    return;
+            }
+        } catch (error) {
+            const message =
+                error instanceof Error ? error.message : String(error);
+            this.logger.error(
+                `Failed to handle review.stage.changed for review=${reviewId} stage=${toStage}: ${message}`,
+                error instanceof Error ? error.stack : undefined,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/user-created-notification.listener.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/user-created-notification.listener.ts.html new file mode 100644 index 00000000..fc0c6ff0 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/listeners/user-created-notification.listener.ts.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for notifications/application/listeners/user-created-notification.listener.ts + + + + + + + + + +
+
+

All files / notifications/application/listeners user-created-notification.listener.ts

+
+ +
+ 87.5% + Statements + 14/16 +
+ + +
+ 56.25% + Branches + 9/16 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 85.71% + Lines + 12/14 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +333x +3x +3x +3x +  +  +3x +2x +  +  +2x +  +  +  +3x +2x +  +2x +2x +2x +  +  +  +  +  +  +  +  +  +  +  +  + 
import { Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { UserCreatedEvent } from 'src/contexts/identity/application/events/user-created.event';
+import { ReviewEmailNotificationService } from '../services/review-email-notification.service';
+ 
+@Injectable()
+export class UserCreatedNotificationListener {
+    private readonly logger = new Logger(UserCreatedNotificationListener.name);
+ 
+    constructor(
+        private readonly notifications: ReviewEmailNotificationService,
+    ) {}
+ 
+    @OnEvent('user.created', { async: true })
+    async handle(event: UserCreatedEvent): Promise<void> {
+        const { userId } = event;
+ 
+        try {
+            const sent = await this.notifications.notifyUserWelcome(userId);
+            this.logger.log(
+                `dispatched ${sent} welcome email(s) for user=${userId}`,
+            );
+        } catch (error) {
+            const message =
+                error instanceof Error ? error.message : String(error);
+            this.logger.error(
+                `Failed to handle user.created for user=${userId}: ${message}`,
+                error instanceof Error ? error.stack : undefined,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/index.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/index.html new file mode 100644 index 00000000..a066fbd1 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for notifications/application/ports + + + + + + + + + +
+
+

All files notifications/application/ports

+
+ +
+ 50% + Statements + 1/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 50% + Lines + 1/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
mailer.port.ts +
+
0%0/1100%0/0100%0/00%0/1
notification-log.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/mailer.port.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/mailer.port.ts.html new file mode 100644 index 00000000..14b770cb --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/mailer.port.ts.html @@ -0,0 +1,163 @@ + + + + + + Code coverage report for notifications/application/ports/mailer.port.ts + + + + + + + + + +
+
+

All files / notifications/application/ports mailer.port.ts

+
+ +
+ 0% + Statements + 0/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
export const MAILER = Symbol('NOTIFICATIONS.MAILER');
+ 
+export type MailerAttachment = {
+    filename: string;
+    content: Buffer;
+    contentType?: string;
+    /**
+     * Content-ID for inline embedding. When set, the attachment is rendered
+     * inline by mail clients via `<img src="cid:<cid>">` references in the
+     * HTML body. Used here to embed the brand logo so Gmail (which strips
+     * data: URIs in <img>) still displays it.
+     */
+    cid?: string;
+};
+ 
+export type MailerSendOptions = {
+    to: string;
+    subject: string;
+    template: string;
+    context: Record<string, unknown>;
+    attachments?: MailerAttachment[];
+};
+ 
+export interface MailerPort {
+    sendMail(options: MailerSendOptions): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/notification-log.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/notification-log.repository.port.ts.html new file mode 100644 index 00000000..c993ec22 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/ports/notification-log.repository.port.ts.html @@ -0,0 +1,193 @@ + + + + + + Code coverage report for notifications/application/ports/notification-log.repository.port.ts + + + + + + + + + +
+
+

All files / notifications/application/ports notification-log.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { NotificationKind } from '../../domain/notification-kind.enum';
+import { NotificationLogDomain } from '../../domain/notification-log.domain';
+ 
+export const NOTIFICATION_LOG_REPOSITORY = Symbol(
+    'NOTIFICATIONS.NOTIFICATION_LOG_REPOSITORY',
+);
+ 
+export interface NotificationLogRepositoryPort {
+    findOneForReview(
+        reviewId: number,
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null>;
+ 
+    findOneForCycle(
+        cycleId: number,
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null>;
+ 
+    /**
+     * Find a standalone (no review/cycle) notification for a user by kind.
+     * Used for user-scoped notifications such as USER_WELCOME, where the
+     * dedup key is purely (recipientUserId, kind).
+     */
+    findOneForUser(
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null>;
+ 
+    create(log: NotificationLogDomain): Promise<NotificationLogDomain>;
+ 
+    markSent(id: number): Promise<void>;
+ 
+    markFailure(id: number, errorMessage: string): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/services/index.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/services/index.html new file mode 100644 index 00000000..8012c947 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/services/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for notifications/application/services + + + + + + + + + +
+
+

All files notifications/application/services

+
+ +
+ 83.87% + Statements + 104/124 +
+ + +
+ 63.54% + Branches + 61/96 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 83.6% + Lines + 102/122 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
review-email-notification.service.ts +
+
83.87%104/12463.54%61/96100%9/983.6%102/122
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/application/services/review-email-notification.service.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/application/services/review-email-notification.service.ts.html new file mode 100644 index 00000000..ce9f40b6 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/application/services/review-email-notification.service.ts.html @@ -0,0 +1,1264 @@ + + + + + + Code coverage report for notifications/application/services/review-email-notification.service.ts + + + + + + + + + +
+
+

All files / notifications/application/services review-email-notification.service.ts

+
+ +
+ 83.87% + Statements + 104/124 +
+ + +
+ 63.54% + Branches + 61/96 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 83.6% + Lines + 102/122 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +3943x +3x +3x +  +  +  +3x +  +  +  +3x +  +  +  +3x +3x +  +  +  +  +3x +  +  +  +3x +3x +3x +  +  +  +  +  +3x +3x +  +  +  +  +  +3x +2x +  +  +2x +  +2x +  +2x +  +2x +  +2x +  +2x +  +2x +2x +2x +  +  +  +  +  +  +  +  +  +  +  +7x +7x +1x +1x +  +  +6x +  +  +  +  +  +  +  +  +  +  +  +6x +  +  +  +  +  +  +  +  +2x +  +2x +  +2x +2x +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +3x +3x +1x +1x +  +  +2x +2x +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +3x +3x +1x +1x +  +  +2x +  +  +  +  +2x +  +2x +2x +2x +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +  +  +  +2x +  +  +  +2x +2x +  +  +  +  +2x +2x +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +2x +2x +  +  +  +  +2x +  +2x +  +2x +1x +1x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +15x +  +15x +  +  +  +15x +  +  +  +15x +  +  +  +  +  +  +15x +  +  +15x +7x +  +  +  +  +8x +2x +  +  +  +  +  +6x +  +  +  +  +  +15x +1x +  +  +1x +  +  +  +14x +  +  +  +  +  +  +  +  +  +  +14x +  +  +  +  +  +  +  +  +14x +14x +  +  +  +  +  +  +  +  +  +  +  +  +  +11x +11x +  +  +11x +  +  +3x +3x +  +  +  +3x +3x +  +  +  +  +15x +15x +15x +15x +  +  + 
import { Inject, Injectable, Logger } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/respondent.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/review.repository.port';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/reviewer.repository.port';
+import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service';
+import {
+    IDENTITY_USER_REPOSITORY,
+    UserRepositoryPort,
+} from 'src/contexts/identity/application/ports/user.repository.port';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import {
+    STRATEGIC_REPORT_REPOSITORY,
+    StrategicReportRepositoryPort,
+} from 'src/contexts/reporting/application/ports/strategic-report.repository.port';
+import { NotificationKind } from '../../domain/notification-kind.enum';
+import { NotificationLogDomain } from '../../domain/notification-log.domain';
+import {
+    LOGO_BUFFER,
+    LOGO_CID,
+    LOGO_CONTENT_TYPE,
+    LOGO_FILENAME,
+} from '../../infrastructure/templates/logo-data-uri';
+import { MAILER, MailerPort } from '../ports/mailer.port';
+import {
+    NOTIFICATION_LOG_REPOSITORY,
+    NotificationLogRepositoryPort,
+} from '../ports/notification-log.repository.port';
+ 
+@Injectable()
+export class ReviewEmailNotificationService {
+    private readonly logger = new Logger(ReviewEmailNotificationService.name);
+ 
+    constructor(
+        @Inject(MAILER) private readonly mailer: MailerPort,
+        @Inject(NOTIFICATION_LOG_REPOSITORY)
+        private readonly notificationLogRepo: NotificationLogRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviewRepo: ReviewRepositoryPort,
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondentRepo: RespondentRepositoryPort,
+        @Inject(REVIEWER_REPOSITORY)
+        private readonly reviewerRepo: ReviewerRepositoryPort,
+        @Inject(IDENTITY_USER_REPOSITORY)
+        private readonly userRepo: UserRepositoryPort,
+        @Inject(STRATEGIC_REPORT_REPOSITORY)
+        private readonly strategicReportRepo: StrategicReportRepositoryPort,
+        private readonly cycleService: CycleService,
+        private readonly configService: ConfigService,
+    ) {}
+ 
+    /**
+     * Send a welcome email to a freshly registered user. Triggered from
+     * the identity context when a new user is created.
+     *
+     * Dedup is per-user (regardless of review/cycle), so re-creating the
+     * same user-id (which doesn't really happen) or replaying the event
+     * won't cause duplicate emails.
+     */
+    async notifyUserWelcome(userId: number): Promise<number> {
+        const user = await this.userRepo.findById(userId);
+        if (!user) {
+            this.logger.warn(`User ${userId} not found for welcome email`);
+            return 0;
+        }
+ 
+        const sent = await this.dispatch({
+            owner: { kind: 'user', id: userId },
+            kind: NotificationKind.USER_WELCOME,
+            recipient: user,
+            subject: 'Welcome to Intra · Feedback360',
+            template: 'user-welcome',
+            actionUrl: this.buildUrl('dashboard'),
+            extraContext: {
+                userFirstName: user.firstName,
+            },
+        });
+ 
+        return sent ? 1 : 0;
+    }
+ 
+    /**
+     * Notify the HR responsible for the cycle that the strategic
+     * (cycle-level) report is ready. Triggered when the cycle reaches
+     * `CycleStage.PUBLISHED`.
+     */
+    async notifyCycleStrategicReportReady(cycleId: number): Promise<number> {
+        const cycle = await this.cycleService.getById(cycleId);
+        const strategicReport =
+            await this.strategicReportRepo.findByCycleId(cycleId);
+ 
+        const hr = await this.userRepo.findById(cycle.hrId);
+        Iif (!hr) {
+            this.logger.warn(`HR ${cycle.hrId} not found for cycle ${cycleId}`);
+            return 0;
+        }
+ 
+        const sent = await this.dispatch({
+            owner: { kind: 'cycle', id: cycleId },
+            kind: NotificationKind.CYCLE_STRATEGIC_REPORT_READY,
+            recipient: hr,
+            subject: `Strategic report ready: ${cycle.title}`,
+            template: 'cycle-strategic-report-ready',
+            actionUrl: this.buildUrl(
+                `reporting/strategic-reports/${strategicReport?.id}`,
+            ),
+            extraContext: {
+                hrFirstName: hr.firstName,
+                cycleTitle: cycle.title,
+            },
+        });
+ 
+        return sent ? 1 : 0;
+    }
+ 
+    async notifyRateeSelfAssessment(reviewId: number): Promise<number> {
+        const review = await this.reviewRepo.findById(reviewId);
+        if (!review) {
+            this.logger.warn(`Review ${reviewId} not found`);
+            return 0;
+        }
+ 
+        const ratee = await this.userRepo.findById(review.rateeId);
+        Iif (!ratee) {
+            this.logger.warn(
+                `Ratee ${review.rateeId} not found for review ${reviewId}`,
+            );
+            return 0;
+        }
+ 
+        const sent = await this.dispatch({
+            owner: { kind: 'review', id: review.id! },
+            kind: NotificationKind.RATEE_SELF_ASSESSMENT,
+            recipient: ratee,
+            subject: 'A new 360° review has been created for you',
+            template: 'ratee-self-assessment',
+            actionUrl: this.buildUrl(`feedback360/surveys/${review.id}`),
+            extraContext: {
+                rateeFirstName: ratee.firstName,
+            },
+        });
+ 
+        return sent ? 1 : 0;
+    }
+ 
+    async notifyRespondents(reviewId: number): Promise<number> {
+        const review = await this.reviewRepo.findById(reviewId);
+        if (!review) {
+            this.logger.warn(`Review ${reviewId} not found`);
+            return 0;
+        }
+ 
+        const respondents = await this.respondentRepo.listByReview(
+            reviewId,
+            {},
+        );
+ 
+        let sentCount = 0;
+ 
+        for (const respondent of respondents) {
+            const user = await this.userRepo.findById(respondent.respondentId);
+            Iif (!user) {
+                this.logger.warn(
+                    `Respondent user ${respondent.respondentId} not found`,
+                );
+                continue;
+            }
+ 
+            const sent = await this.dispatch({
+                owner: { kind: 'review', id: review.id! },
+                kind: NotificationKind.RESPONDENT_INVITATION,
+                recipient: user,
+                subject: `Your feedback is requested for ${review.rateeFullName}`,
+                template: 'respondent-invitation',
+                actionUrl: this.buildUrl(`feedback360/surveys/${review.id}`),
+                extraContext: {
+                    respondentFirstName: user.firstName,
+                    rateeFullName: review.rateeFullName,
+                    rateeFirstName: review.rateeFullName.split(' ')[0],
+                },
+            });
+ 
+            Eif (sent) {
+                sentCount += 1;
+            }
+        }
+ 
+        return sentCount;
+    }
+ 
+    async notifyHrReportReady(reviewId: number): Promise<number> {
+        const review = await this.reviewRepo.findById(reviewId);
+        Iif (!review) {
+            this.logger.warn(`Review ${reviewId} not found`);
+            return 0;
+        }
+ 
+        const hr = await this.userRepo.findById(review.hrId);
+        Iif (!hr) {
+            this.logger.warn(
+                `HR ${review.hrId} not found for review ${reviewId}`,
+            );
+            return 0;
+        }
+ 
+        const sent = await this.dispatch({
+            owner: { kind: 'review', id: review.id! },
+            kind: NotificationKind.HR_REPORT_READY,
+            recipient: hr,
+            subject: `Review report ready: ${review.rateeFullName}`,
+            template: 'hr-report-ready',
+            actionUrl: this.buildUrl(
+                `reporting/individual-reports/${review.reportId}/comments`,
+            ),
+            extraContext: {
+                hrFirstName: hr.firstName,
+                rateeFullName: review.rateeFullName,
+            },
+        });
+ 
+        return sent ? 1 : 0;
+    }
+ 
+    async notifyReviewers(reviewId: number): Promise<number> {
+        const review = await this.reviewRepo.findById(reviewId);
+        Iif (!review) {
+            this.logger.warn(`Review ${reviewId} not found`);
+            return 0;
+        }
+ 
+        const reviewers = await this.reviewerRepo.listByReview(reviewId, {});
+ 
+        let sentCount = 0;
+ 
+        for (const reviewer of reviewers) {
+            const user = await this.userRepo.findById(reviewer.reviewerId);
+            Iif (!user) {
+                this.logger.warn(
+                    `Reviewer user ${reviewer.reviewerId} not found`,
+                );
+                continue;
+            }
+ 
+            const sent = await this.dispatch({
+                owner: { kind: 'review', id: review.id! },
+                kind: NotificationKind.REVIEWER_REPORT_READY,
+                recipient: user,
+                subject: `Individual report ready: ${review.rateeFullName}`,
+                template: 'reviewer-report-ready',
+                actionUrl: this.buildUrl(
+                    `reporting/individual-reports/${review.reportId}`,
+                ),
+                extraContext: {
+                    reviewerFirstName: user.firstName,
+                    rateeFullName: review.rateeFullName,
+                },
+            });
+ 
+            Eif (sent) {
+                sentCount += 1;
+            }
+        }
+ 
+        return sentCount;
+    }
+ 
+    private async dispatch(args: {
+        owner:
+            | { kind: 'review'; id: number }
+            | { kind: 'cycle'; id: number }
+            | { kind: 'user'; id: number };
+        kind: NotificationKind;
+        recipient: UserDomain;
+        subject: string;
+        template: string;
+        actionUrl: string;
+        extraContext: Record<string, unknown>;
+    }): Promise<boolean> {
+        const { owner, kind, recipient, subject, template, actionUrl } = args;
+ 
+        Iif (!owner.id) {
+            this.logger.warn(`${owner.kind} without id, skipping ${kind}`);
+            return false;
+        }
+        Iif (!recipient.id) {
+            this.logger.warn(`Recipient without id, skipping ${kind}`);
+            return false;
+        }
+        Iif (!recipient.email) {
+            this.logger.warn(
+                `Recipient ${recipient.id} has no email, skipping ${kind}`,
+            );
+            return false;
+        }
+ 
+        const ownerLabel = `${owner.kind}=${owner.id}`;
+ 
+        let existing;
+        if (owner.kind === 'review') {
+            existing = await this.notificationLogRepo.findOneForReview(
+                owner.id,
+                kind,
+                recipient.id,
+            );
+        } else if (owner.kind === 'cycle') {
+            existing = await this.notificationLogRepo.findOneForCycle(
+                owner.id,
+                kind,
+                recipient.id,
+            );
+        } else {
+            existing = await this.notificationLogRepo.findOneForUser(
+                kind,
+                recipient.id,
+            );
+        }
+ 
+        if (existing?.isSent()) {
+            this.logger.debug(
+                `Skipping ${kind} for ${ownerLabel} user=${recipient.id} (already sent)`,
+            );
+            return false;
+        }
+ 
+        const log =
+            existing ??
+            (await this.notificationLogRepo.create(
+                NotificationLogDomain.create({
+                    reviewId: owner.kind === 'review' ? owner.id : null,
+                    cycleId: owner.kind === 'cycle' ? owner.id : null,
+                    kind,
+                    recipientUserId: recipient.id,
+                    recipientEmail: recipient.email,
+                }),
+            ));
+ 
+        const context = {
+            ...args.extraContext,
+            actionUrl,
+            subject,
+            supportEmail: this.configService.get<string>('app.supportEmail'),
+            logoCid: LOGO_CID,
+            appName: 'Intra',
+        };
+ 
+        try {
+            await this.mailer.sendMail({
+                to: recipient.email,
+                subject,
+                template,
+                context,
+                attachments: [
+                    {
+                        filename: LOGO_FILENAME,
+                        content: LOGO_BUFFER,
+                        contentType: LOGO_CONTENT_TYPE,
+                        cid: LOGO_CID,
+                    },
+                ],
+            });
+            await this.notificationLogRepo.markSent(log.id!);
+            this.logger.log(
+                `Sent ${kind} for ${ownerLabel} to user=${recipient.id}`,
+            );
+            return true;
+        } catch (error) {
+            const message =
+                error instanceof Error ? error.message : String(error);
+            this.logger.error(
+                `Failed to send ${kind} for ${ownerLabel} to user=${recipient.id}: ${message}`,
+                error instanceof Error ? error.stack : undefined,
+            );
+            await this.notificationLogRepo.markFailure(log.id!, message);
+            return false;
+        }
+    }
+ 
+    private buildUrl(path: string): string {
+        const base = this.configService.get<string>('app.frontendLink') ?? '/';
+        const trimmedBase = base.endsWith('/') ? base.slice(0, -1) : base;
+        const trimmedPath = path.startsWith('/') ? path.slice(1) : path;
+        return `${trimmedBase}/${trimmedPath}`;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mail/index.html b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mail/index.html new file mode 100644 index 00000000..e1ce4119 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mail/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for notifications/infrastructure/mail + + + + + + + + + +
+
+

All files notifications/infrastructure/mail

+
+ +
+ 0% + Statements + 0/10 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
nest-mailer.adapter.ts +
+
0%0/100%0/40%0/30%0/8
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mail/nest-mailer.adapter.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mail/nest-mailer.adapter.ts.html new file mode 100644 index 00000000..140096a1 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mail/nest-mailer.adapter.ts.html @@ -0,0 +1,172 @@ + + + + + + Code coverage report for notifications/infrastructure/mail/nest-mailer.adapter.ts + + + + + + + + + +
+
+

All files / notifications/infrastructure/mail nest-mailer.adapter.ts

+
+ +
+ 0% + Statements + 0/10 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { MailerService } from '@nestjs-modules/mailer';
+import { Injectable } from '@nestjs/common';
+import {
+    MAILER,
+    MailerPort,
+    MailerSendOptions,
+} from '../../application/ports/mailer.port';
+ 
+@Injectable()
+export class NestMailerAdapter implements MailerPort {
+    readonly [MAILER] = MAILER;
+ 
+    constructor(private readonly mailerService: MailerService) {}
+ 
+    async sendMail(options: MailerSendOptions): Promise<void> {
+        await this.mailerService.sendMail({
+            to: options.to,
+            subject: options.subject,
+            template: options.template,
+            context: options.context,
+            attachments: options.attachments?.map((a) => ({
+                filename: a.filename,
+                content: a.content,
+                contentType: a.contentType,
+                cid: a.cid,
+            })),
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mappers/index.html b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mappers/index.html new file mode 100644 index 00000000..21b2ba56 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mappers/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for notifications/infrastructure/mappers + + + + + + + + + +
+
+

All files notifications/infrastructure/mappers

+
+ +
+ 100% + Statements + 8/8 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 8/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
notification-log.mapper.ts +
+
100%8/8100%0/0100%6/6100%8/8
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mappers/notification-log.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mappers/notification-log.mapper.ts.html new file mode 100644 index 00000000..c0cb66a5 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/mappers/notification-log.mapper.ts.html @@ -0,0 +1,271 @@ + + + + + + Code coverage report for notifications/infrastructure/mappers/notification-log.mapper.ts + + + + + + + + + +
+
+

All files / notifications/infrastructure/mappers notification-log.mapper.ts

+
+ +
+ 100% + Statements + 8/8 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 8/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63  +  +  +  +  +  +  +  +1x +  +1x +  +17x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +15x +  +  +  +  +  +  +  +  +  +  +  +  +23x +  +  +  +17x +  +  +  +  +  +15x +  +  +  +  +  +17x +  +  + 
import {
+    Prisma,
+    NotificationChannel as PrismaNotificationChannel,
+    NotificationKind as PrismaNotificationKind,
+    NotificationLog as PrismaNotificationLog,
+} from '@intra/database';
+import { NotificationChannel } from '../../domain/notification-channel.enum';
+import { NotificationKind } from '../../domain/notification-kind.enum';
+import { NotificationLogDomain } from '../../domain/notification-log.domain';
+ 
+export class NotificationLogMapper {
+    static toDomain(record: PrismaNotificationLog): NotificationLogDomain {
+        return NotificationLogDomain.create({
+            id: record.id,
+            reviewId: record.reviewId,
+            cycleId: record.cycleId,
+            kind: NotificationLogMapper.fromPrismaKind(record.kind),
+            channel: NotificationLogMapper.fromPrismaChannel(record.channel),
+            recipientUserId: record.recipientUserId,
+            recipientEmail: record.recipientEmail,
+            sentAt: record.sentAt,
+            errorMessage: record.errorMessage,
+            createdAt: record.createdAt,
+            updatedAt: record.updatedAt,
+        });
+    }
+ 
+    static toPrisma(
+        log: NotificationLogDomain,
+    ): Prisma.NotificationLogUncheckedCreateInput {
+        return {
+            reviewId: log.reviewId,
+            cycleId: log.cycleId,
+            kind: NotificationLogMapper.toPrismaKind(log.kind),
+            channel: NotificationLogMapper.toPrismaChannel(log.channel),
+            recipientUserId: log.recipientUserId,
+            recipientEmail: log.recipientEmail,
+            sentAt: log.sentAt,
+            errorMessage: log.errorMessage,
+        };
+    }
+ 
+    static toPrismaKind(kind: NotificationKind): PrismaNotificationKind {
+        return kind as unknown as PrismaNotificationKind;
+    }
+ 
+    static fromPrismaKind(kind: PrismaNotificationKind): NotificationKind {
+        return kind as unknown as NotificationKind;
+    }
+ 
+    static toPrismaChannel(
+        channel: NotificationChannel,
+    ): PrismaNotificationChannel {
+        return channel as unknown as PrismaNotificationChannel;
+    }
+ 
+    static fromPrismaChannel(
+        channel: PrismaNotificationChannel,
+    ): NotificationChannel {
+        return channel as unknown as NotificationChannel;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/prisma-repositories/index.html b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/prisma-repositories/index.html new file mode 100644 index 00000000..34a76ad8 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/prisma-repositories/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for notifications/infrastructure/prisma-repositories + + + + + + + + + +
+
+

All files notifications/infrastructure/prisma-repositories

+
+ +
+ 100% + Statements + 19/19 +
+ + +
+ 90% + Branches + 9/10 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 17/17 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
notification-log.repository.ts +
+
100%19/1990%9/10100%7/7100%17/17
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/prisma-repositories/notification-log.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/prisma-repositories/notification-log.repository.ts.html new file mode 100644 index 00000000..2b2d9a5c --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/prisma-repositories/notification-log.repository.ts.html @@ -0,0 +1,364 @@ + + + + + + Code coverage report for notifications/infrastructure/prisma-repositories/notification-log.repository.ts + + + + + + + + + +
+
+

All files / notifications/infrastructure/prisma-repositories notification-log.repository.ts

+
+ +
+ 100% + Statements + 19/19 +
+ + +
+ 90% + Branches + 9/10 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 17/17 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +943x +3x +3x +  +  +  +  +  +3x +  +  +3x +3x +  +3x +  +  +  +  +  +  +12x +  +  +  +  +  +  +  +  +  +12x +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +13x +  +  +  +  +  +  +  +  +13x +  +  +  +29x +  +  +27x +  +  +  +13x +  +  +  +  +  +  +5x +  +  +  +  +  + 
import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    NOTIFICATION_LOG_REPOSITORY,
+    NotificationLogRepositoryPort,
+} from '../../application/ports/notification-log.repository.port';
+import { NotificationKind } from '../../domain/notification-kind.enum';
+import { NotificationLogDomain } from '../../domain/notification-log.domain';
+import { NotificationLogMapper } from '../mappers/notification-log.mapper';
+ 
+@Injectable()
+export class NotificationLogRepository implements NotificationLogRepositoryPort {
+    readonly [NOTIFICATION_LOG_REPOSITORY] = NOTIFICATION_LOG_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async findOneForReview(
+        reviewId: number,
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null> {
+        const record = await this.prisma.notificationLog.findUnique({
+            where: {
+                uniq_review_kind_recipient: {
+                    reviewId,
+                    kind: NotificationLogMapper.toPrismaKind(kind),
+                    recipientUserId,
+                },
+            },
+        });
+ 
+        return record ? NotificationLogMapper.toDomain(record) : null;
+    }
+ 
+    async findOneForCycle(
+        cycleId: number,
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null> {
+        const record = await this.prisma.notificationLog.findUnique({
+            where: {
+                uniq_cycle_kind_recipient: {
+                    cycleId,
+                    kind: NotificationLogMapper.toPrismaKind(kind),
+                    recipientUserId,
+                },
+            },
+        });
+ 
+        return record ? NotificationLogMapper.toDomain(record) : null;
+    }
+ 
+    async findOneForUser(
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null> {
+        // No unique constraint exists for (recipientUserId, kind) when both
+        // reviewId and cycleId are null, so use findFirst. Welcome-style
+        // notifications are dispatched only once per user, so a duplicate
+        // here is benign (worst case: a second email if a race occurs).
+        const record = await this.prisma.notificationLog.findFirst({
+            where: {
+                kind: NotificationLogMapper.toPrismaKind(kind),
+                recipientUserId,
+                reviewId: null,
+                cycleId: null,
+            },
+        });
+ 
+        return record ? NotificationLogMapper.toDomain(record) : null;
+    }
+ 
+    async create(log: NotificationLogDomain): Promise<NotificationLogDomain> {
+        const created = await this.prisma.notificationLog.create({
+            data: NotificationLogMapper.toPrisma(log),
+        });
+        return NotificationLogMapper.toDomain(created);
+    }
+ 
+    async markSent(id: number): Promise<void> {
+        await this.prisma.notificationLog.update({
+            where: { id },
+            data: { sentAt: new Date(), errorMessage: null },
+        });
+    }
+ 
+    async markFailure(id: number, errorMessage: string): Promise<void> {
+        await this.prisma.notificationLog.update({
+            where: { id },
+            data: { errorMessage },
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/templates/index.html b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/templates/index.html new file mode 100644 index 00000000..0104bfb7 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/templates/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for notifications/infrastructure/templates + + + + + + + + + +
+
+

All files notifications/infrastructure/templates

+
+ +
+ 0% + Statements + 0/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
logo-data-uri.ts +
+
0%0/5100%0/0100%0/00%0/5
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/templates/logo-data-uri.ts.html b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/templates/logo-data-uri.ts.html new file mode 100644 index 00000000..3d8c09c8 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/notifications/infrastructure/templates/logo-data-uri.ts.html @@ -0,0 +1,190 @@ + + + + + + Code coverage report for notifications/infrastructure/templates/logo-data-uri.ts + + + + + + + + + +
+
+

All files / notifications/infrastructure/templates logo-data-uri.ts

+
+ +
+ 0% + Statements + 0/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// Auto-generated from apps/web/public/logo.png (resized to 80x80 PNG).
+// Inlined here so email clients don't need to fetch a remote image —
+// the logo is attached to every notification email as an inline (CID)
+// attachment and referenced from the HTML via `<img src="cid:intra-logo">`.
+//
+// We attach via CID rather than embedding as a `data:` URI because Gmail
+// (web client) strips `data:` images from the rendered message for
+// security, leaving a broken-image placeholder. CID attachments render
+// reliably in Gmail, Apple Mail, Outlook, and Thunderbird.
+//
+// To regenerate after the logo changes:
+//   sips -z 80 80 apps/web/public/logo.png --out /tmp/logo-80.png
+//   base64 -i /tmp/logo-80.png | tr -d "\n"
+//   then paste the result as the value of LOGO_DATA_URI below.
+ 
+export const LOGO_DATA_URI =
+    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAUKADAAQAAAABAAAAUAAAAAAx4ExPAAAdO0lEQVR4AeVdC3RcR3n+5967u9LqYVt2/JJt+aUkfjuxSwgchxBMDoXwSEtSkpIDKQ0UUlpKWw6nxLUDgcM5gbYQmvJoIAcILiQ8GhICcYgbJxBCYju2Iye2ZcuWbMmyLcmyXrt7H9Pvn72ze3f37u6VbKUFxudq7s6d+Wf+b/6Z+eefhwVFcFI+N52G7dlEccROnBYNa05FSPYHEUWU41LK7iSl2t5DnvceyngryTWmk2MQydgAebE2gPljqm36jmhaMliOxh9CeCiA8twTV1LC/jIJYz3ZgCGFx0ZU1ySAhx8JIgE/LV4i6f2tWLruyQsBljzQPYOks5AcOYdcGSfLGaHZpyTFKIl8UuSJHpra1CXEsr4LkV+QxhYpjc2nXriShPNGEvaleKZQTAyQKfYQWU+I+g3wS10JgHLoF9dQ3HmQyGiiYReAWVIBx+A5eNjXj1EjIJXDiHuLWL78J6Xkq4fIV15poHTiOnK9m8hz15NLM8kykQmKZiD/+Scg7PBdicd1yDBPkTCfRwV+j1INj4rZa0aq51I5hjyy6yoy5WaizNWUFAYZDrLHE/PwIG/HTpNlPUyZ2jtE44aDQWoFAMrBp1opNvQUCTGHxiSAs/JgFYOnARUJgeY9SPXGNaK1dVeQeLV3uafjPWS7d5BhrFBxbRRaotD4izBBMfMAtXSaJDNLyUN5GFUEUwzl4l+SpSNxl5j6+oc40USc3L/vA2R691BM1pIzCmHzJJkAzcLDfvYR1MBYOCfIrrlZNLxph84LnVrWSSkFWaOfp1gsGngMrocnDcZkYgr1i7ul3I6A6k52dyflzvavkeNtRewVlMlI9UgUPgsNpA9CKEUXKrOLzHwxFZBpG/HxkLuG5NiDcmD7V6Tsqq2ec2EMuffgzRCSr1JG1NIYRLygtTFgudYmaQj5SdFMsdRDcmTH5ZpSrmSUeXYlmtB1GG2RMJC4nOS5SKq/jUJy3PjVtGvJlZpwOV+2naqnnpGtZMY+SDbychwGrdQxaNLYh1p6ibhFhzkbTDvIOy5up4H2rfJUW31YtLAwuburldLGl8k2LHJEvpvSoPGAqflTPjAZQwUL4yKSQ/dJmc0rD2BqZCPFauLkVOjzdLP1wJAiiuRKEk2AkDQoFXtbWGF1GKTcpNTgvWTF30Gpsby06QhBX6CpSmonS74MKQx+KX6XNJqSlBDvJPPkPcgjz1NxzODvlPePJJLTKY1cCoDypY551PxqUJnXEYAYt9bSUPdNTC6fmeetUGqKjqyJBn87IMDNtvgbE86AWlqsDJax5H3X4Q+TEbuFxhR4JZ8LAjz0hUJ0otmcQLMt+BT6g0GMi/fT4NMfCP0eCJQHzs2gtPkOGuOWg7Lzwzxp0DTP5Xzm1XPfy5WVB9AxGiDOIagHaiQneRzmZ6wy8dPZVtkmJPccn4eRdhPZnHtVh9Hd8TDadkF16aYMN3P00dUcD0JuerMc/s2silHPppYBrFno+7L8aqBYQDSYOkz7WmjYZ7VO0qU0vHt6HkDPGg2XLiRQiZk4A+eDVeD7GXtGumzB3fRtFE/MhGIeQZy4+coRMqkPA00P3oeqNONsti5oJ6xmyqRvLVsO/jBKc4kw5njoejRASiDAm5ZEDVjuu4+DEiKOJxpIjkzLA+hahxWAxQlyzZYT4cl914DC58yg92LMPxRWcHnyZB2AgMoSSfrQsShh66dZzQM0fcZZgDcAtSaMdGmYUoXsm6XsqCn96Ic48Wy/5yAj5ofLrwBjnphP+OWacw5YpE0LKI3ayZrtNAolTBNlwkzEY4J4CoDzM8pnLoi1ClM8rskV+KdSq6HBLVUjZsGHMj8YLEP00izICq0ew48zPqhlEgSCeVQmbzkNnijfH5tmryqvkjrwqHkrBlGHl/gMsDWM52wewLnrnwP6z5CVDNSIrhnOhEHTmTGg+J2tOSi3nMZ5iWaN/jLASv7Vc1aTFTMgSRGaL5IxgIJOCMTHg9FEnogsgYQ84jEu6Jp8AYreLOMgZjz9mNdneSqRNl9AtLRpX/Mv0No86wDNvOJ0DkAUFFVXuwkdKwb2mFDSl0voE8yBGACP58YumEyIT4k1s8OnVZIW88wssuOGIcSxfHyjMzqASMV5eV5rPn3hm7iqrgf0HyejjltXthlrcAp49gVI9XtaeOAL2ALcmoe4cnMAchZi8eodMBbcDs3UweyikLACD9E5I86EM+Z+zwAxM/NJ8cd1DxcWM/DLlYsiqSI6CQMgxXH9E8prAMxcaPkXng5KaikfAV+Sxt1kO2PgM8uPBjDHHwPGguPzq9U3nrqOuu00dfa3mX4BgBwAo8B9mAG8DxJ4kmINAAkJHNMHU0keA4dp3xREjg0CvNvF2+vv5rRhTk0RJc2DRIR9Dg/juCakTjvTgDoTrfWrJGx4IG8+14ImUeyLN9XtItP+BKnmiBaX45GBCz4aREAlwLdrjpKV+IiYMr+faZYAyIFi9cXfo7rklZg8fxFlOILZO5o3gCRYXzzLw8DShQn+NyiZ2SCuq7uX05R1vb3oICXUF0zMoziek/P0zjJghvGdYXZDWkCmPCA6qvK5AqQ7S8qeivNj8faGr5CRZhBd1Y+7vqDoPlE3Z8cSZNYBPAsagXmLWLxqm84P8IY7sWzOUXz5B3mo79N0xltKYwIWaXRO9dZJsuzDYkP9QHjKotBBeyp51ASOij6U+ammcN6YMlvpKCnkKeQoRmLYBXVgBV9JuzGDxjqRL4/k5R23HvnIuQPkxbeQqL2MLBRAQN1ik5aJisDMltxRFy3gCczJN4llFz8fpFYWQB1JtE4/h/dxmal0WuU7zkUAD0pnFM6RIqvv9VPSOZOjE0+eIWdogIQHACPQ4ShC1kNPm4634zk6ZV7EdY0Py193baORpmvI9jYin4sxa0rAtDZEprOP6ozHxGXzfoPBqSTzqgCWyTN6cMZrRs3B4lHG6lJMSamm3mlauHAo92nKqnN0pvcU+sXmiF0BbHrIU6aaQSPUkpyj7b+I182HvkmP+k/x57K/Q/vAsrEn8sGT8wi8RHZsxhKCdcBcp6neEYbmxZIVjZSFgYDEgmiRJx5rHJxNNBO5YPw6ICy/xU7GTsBchrUZCApbiQ0AyTo2Ox5rg7jyWMNh5P0+AGgsiNRvMb/smHFhHuXXAjdU30lDGBNSMIVw587rJWx2Vw86fIsf/Db9zl9h67UU0JiEHxdAAqWQpw/UU/3wNFhBZmG0mg/1Zx6aGiQP78dGr6FMTVA+KrPB+p6gzpJIbuyY0gVZuli9kCh6se1Hgc/g4onxaCPfIl849CDo8bJAdnnAwhKBUdMLy9AALZ42xLOJkrzGETAuANVa8dDRtVCKV5ObuRj5oIZ/ORej1WwaoWlqtI1ZBmHaq5wuGpuwVJOqUjKlA8LMX4PmWuy8xDGsjmFdApR4NCzXFzLAavYAAkI0wXr8bvhZai4KlML6C2WGoRINUP+ZXqzNYJ6NaaNhHlRLmE2x3WLu3IqqT7BokQCUfb+dT8bYB2jg5RuhoF5KCWju3KGzvsUSw0Ap5ZXLxmYZ3ylGeFDIB+lPob4agWH7k7BEFzvTPIY143MAA1OgCPQUZpDCTLGYclWyLY8aoDKhlRh/BJHNkhxNS0rbL8u9R7dS3PimuHRBd3Exin/7olIcrH+jefZv/xCJc89R3NtMwoUl1xU0hoz44dUxXthhQyb/CyLFoLFtrXC6rQmH+9nl4A6yRkolcMkSGADkYawZh6eNHoqCAVhuFaxa8YpgGssBKTwuePPkcljBP0PDmefk7vbbqpEtCyCvcMmBJ+/DOulXgcIc4tpxWNyiihOyZgn0/OZTrST8ndd7DfG4WLGixPKKvsqBbrcNC9xRKE0sDpvbXAgEL3ix+iWNr6OJf52XYcsRDAVQdr+QJOPkAxQ3b6U0eGEJm4hjyy63mGgORlnsAEgkv1s2ep21FVZtbpORiZalVe2DByAz4N2M3UY9o9+VXVh4D3GhAFL87N1Ui1UrXukaj8QFM2AWFYDBwArvNbDAC3pIrJxXduYgLlm8B93I94njvjouK41m7HrqPfz5sCxLAJQDT1+PAeLDAC8sfvQwltmoAJqwgjhOD3ZFbKqaQTLxz+RkujG7mXwp1IVJ8SxP3I7B5a06SPsFACo1xUvdiU6WCzexZqsps8/6WjXHe2CEsNG/fUSsWdhRLbpYvvgYWfQhDJxpjKKvFoiMhYnu7K7iplwAIA22vxU63CrY3s4fPN4gUE0CWYoMI4Ome7u4bFHk3V1i7cWPYHT6KyjH6VdNElkHNazL6OyxtwQruRBAx74pp3QGY03kndUXVmNCB21otjUwzvIMISZvEOuWfmO8WYh1F98P2jdCErsULe5BJ9uxwKcyNwazyQEoB3ZPhci8Bopw8PvE3hm0Qh2QmymUbyjgDJwlRkja3yIDFu01rQ9PLBMgdjnSCmMDMrsPXcAo1dSCNqSa85oMQHkzlJRXyFPYIOW7XK3J/l+vIm9oJyLA5DHu/o+lCToc6oNrCWMCpbHidWIW6gQTfN4YKcRZfDuEJvcYNi3+SKxobdOFuBC+3N+5EltA/gyK/bWYWi4BB9MwKGEp1ZcRXmjiqRz7rJXxVDnEQFqlLFw5KaptuEysnvsKx81rpcJpwseYmpLpuWOeWgAgH6Ss5ThbqIyNUmGnqsRaqyNOYc21k9zUUZrS30GjiWOY3PZQ0u65s+UHvVvEFsS98E4sX/ASqL6ERazNtLtrNkzyUIQxV/dc+FgVFLIFgM1DnJnwMR3EVC6ORaIcwABUTU19kBngYkHiILQhmPjZ0q1cAECAyVIEW4BqBhogliCb/4hhANOHQp3EHmaeVgEYtmzEjlIscZykeQazjv47v3LF8JYt52fh8Ms2Ic+3rvAclp/fBokAXJM6OxvprJwKXmdjZtUMCYCV2wPQ2JfNsw8SaDa8hgOQLUx7tDGYF8V4YYvnzVgF13TzANowqBk1gN99GYTboLC+gqbYAaB6qCZ+HCNQPw0mB8Xc9ZEtFTqT/y8+wAUKxIth/HSElUt2YE/NQA2v4czA+shsGDAWId4StM4VsBJdAfRmQVPJDRR5AHtnGoSeCujGIWlQ9Y16GDYbEdKHviuJXU/DlBiDlU2afkHC8v/dDmNz2plfxSCbtdAy62BXbESfiebuTYNRBO/YzcrNmAdI3+UBdMw4OmAAiC0RltmqBgQeTblf4JE5g+1mwwZsaIfOyOcPsoGSmzBsad5xLLB3Y2TtpUTiNC2ZNQiAS2xIOsP/S1814cF9EIrh6RhQ0E+i2ZK7EE0XVnM0575fNiNsFmybTfjdAKHBgAiwGDQJDDAWqj4zVptDMA8gL4Zyx8kbvXlMUAdEfHa5ZoSogzRiaDWwSGSsVYS4z1T6HoinvTSNpAdp4FwfLBjof3Cmg2Qn+tQOdBudmJR3YIN2J3byTyq4W+QWY/PR982EeaoZlqR5lBhdTDV4DHsh9T3RjBLPRLmgsoEXNo3pvp5Z5tEZNgS84IHP5rpCB44hVaaZsxblATTcMzCWAgkca1GpAyn1riqWRibOPUmpSwBgjHAGCiiWKYBZpeEk3Pl6Y1gYN47I3Ye3UY31gFjWsrOUxMRDZNuhFajzG2k3vZk8HItwcc4lFjNpDEaUZvAbB2s8BmjDLwtLupKViaWj2CEM1kPwx32ocrlY8vBhHFVw9yLCAmRSjLyOfz4+tkegxmOoHxt2ItN8iBJys1ixtP18iMo9RxeheW2CZnAjNq9jIydql5uaVkO4G1KHdQCgmuKfR24mJMLF4DOjeTXGFBwwUmNylqBYwmfexLOKwfPIo0JS31jJll8vjgHqZuw13iF3HvyTCmkqfpK7D7wLYD2NWrkVANYpq7JaN1HoKRt59qAMmsH5gsclYYOvMJ/V4HFQrjPkHzCgPqBEXP2Y1D9ZMzrrXmRujWI6Ly6N3HX4L7DR6b/QZzUr4HyRK4yHBqaWQLkfuQCO+0cz8f0gpUIAk4seR43+FlKYa9rByBf8nc3njstq07/LF6NLonyx/Xrg9R+YIiaUCb5SwXi9mJvx+bo45ti23ElTmx8PkioAULRC/YjF7kDhuLt9dUDkUZ/n3w7dK/cdWhIsXNi7bOtaCka+hhEzHqmv5sX283esH2MLXOIOIRYVWJoLAOR8xGWLt2Go+iIsG+efbVQKLIlWAuc2aHPFJKxOjaU+C6vORVUlTxNSEqh/TNBPYubmmf8mpm/4eTGFEgBVhHWtm8i1v6PMQ6+WJLLZ3JPvlvvby+6uh/Sth5ZwPZYhi/ko/5sBnHgLFpSE+S2FdZimeZ8KyyQUQMwkbLp8yV9i98G/YkIN6XhV1h9wsDqOKZS8OaygKiyVfj+6mFI9tVwC7vsm2oQtqCw86qa9e6lpyfuFCJ8AhALI5QGIGbGu9ePon9gCe5ASbAhVg8vk9Y18xsN1r8WUK6/g++DIF7A263kb1QlPP6yqxwDyTq7ojs8oC6rl7cyYQXmx94qma24v7veC5CKBIY8dm0b9zk3Q325BM1sLIGuU4ZTVLda5i+1oeuYSzCnSOwyRhhykZmuVaF7aFUwi93ZcSmOZ3ahQXtNEphEcSx8r0bzZKO+QB+MD2dE+2z+Zl4xjYwa1D88DOEX/XTH79afyycLfSmo6LJpoaeGpy72QjK/Rns5LARis185SZLoIo9MczJ/nYszGVl7Y0AwTc0xIqjbK8sI8j7RRqoqZEKKRhtMtoFUAIFQR7M8xYMxUk9WwYhaGseJsYVtEEi2e82baPKV0Jd+9gEmDPI1xtRe1fwLfj8KueRij7F6a+ro21YUVUiv7KxKAOjUIc3tgU3yBOV41uf3HGymVbsLi33wANgf6Hba4iaVUm347OMiejGTDAzMWNivg5sYmdgbfyCxEHs/gyTvHbUFfzE08H1bpTRkJZC9l3J9Coo6A9gko3icoAdvmiNdPs6fDalS6haQSybBv4wIwjACHAVjWG/ncBD/teHJO9j1xP2Y476NRWDZ4r4zasYVsec3YZp9B8cPZzsbNKpZemCOgX6RoUd/072o+K75u8jExfeNt1aKez/cLAmDFAggT9kLE4H4o2KGrZuWn5DVk3oTEQMZY/7QX+l/ynsSaBjfDqE51IbzsMLlu8gEknDiSfrMLNt0CLPCD9zwzwHEAnTEWlrA93tNOCmxj0gFE1U+2k11YY4mWCQOsxgg5B/1qXCfCO85s8KJPRDqcMHsZxXFNY7L8yQdQ4JiWwwY61ZCr88FqEWFBZ3gvW42z7nDvFAwvTeMAEMORB5ujBav45LqqTVjuPTsNO7UuobH4DHV60cIiU6NoF5c3nI5UtHjdaVijzykAovRhDKDwpmJrLqtFWT0sjXeJMMY2isuqUIOwLEUrI2jKX4214FqTVdjtvwBdicDS2lFqEC+J5dMqdgNlAcS6BsziiY/S4Mi7cBkFVBNIEDcv2ENpONYjHx35OU5qfkm8ZVrZ/XyK1+TsszTW1w/LY1O2eVZFAJt4cIQy5jKAWedC1zSw6BVVB2QVRso+WrnorCZRzpfbRl+L+2M+Tn32m7FcCakHJFxRLvTXTOasfP74NorLL4o1858LoxHahOWL+2+BGPwai0d/g/XhBRgV0SQYOTyssUh3Doka7F5N7pCPjHwwjLAOEzQHZ3KhsOrFG/2hks/6nuPMz0Vx3LlKB8wFVHnhvLCeDfWq4gKWfHT072jU2k7CukFJvYetve5w9lH84qCkoBuwBLFdvnjg78NyLQFQtu29Fcx+C0BdRDaICWzC1vdJ8SEWC2JoIMxmZd5pxI1FX5WPjH4sjLgK42mdYXSPS4djXdANnDKSOPurZzZlMwp8UJUVOC4b+KRf5X8Pf4Kc2L9gPQWzm3PQAqCn8tRPH9RRp6Gwzc8ZgeDYtRDML8i2tn/S6bVfAKDsarsCt5jdA9BMXP+WvYSLCSmiAI/N4/ybM2JQuYJlms/RfkH+bAQzjnJOHB2XBHJfafCB6ZyDDph7r/7CYAvkWcbJn4+8DfrS5wgrsTgvzHz54DGvzCPzyg//5gdY4PY93Ob2Gdmx911BsjkAoSqYOH/yOayUYf0XCfi8LM5WZ2tFE9JE+RvCFIjoKwycUM7Iz8vt+W1fwUwQ8VDh7yq/eCBhxVk7150/jhGY0yJLeUAnD/o41lqLnugu6J0mBgwIiQYvwJsGTvEPDPhYmclxcTORN/pptZPXJ5oDkE7uWQdQ3pC9Ao7BUYmQ0Jc6TVT5fi1pECWMoVZsOY50bQwWNvduWXuwSA2CQU0697X0Ra3dejhxLg1VsQKHvaPrgAKdP28Z3ltKGCHD0zagNawlnN/OCkk5Pn0eWRIVFojn4ahHnFZRT89VmnYeQDqHywdx5xzXSAFYTCAIItcUP35YljjfMslK8LWacIHfOH8fsDsQeSBQYOGaAGqvpyMD9ZAovvGogGTZHzwACdysuXLRK6FxXGcjJTBlNNGXa2DCmi1/Y8lT0scg+w+s+yRSb9S08wCa3iXZY+6ByDqRqgUGrBhIjuvXkLp9DFcDhDghcJjZsL6nLLwh30uCVBPGhp7UEJ4UlGiJZxwAmuZ3MAKjbwlxEnt/YHDPtzCfhxyv+K3A8/nKgezjwhuzLOcSTTkPoOHg7JifKEhMv+e+BUFkovyb04GwiWPy5ZxrfpNSNo4n8BBbxXEfZhq1UJ1gBnNmwRwV7ag/b1rPZI7gMoxvl83BxFKoyWVFmQsEwueFW5s+Sqt5174GVmZyK255AIV3Tt0ZypGLUQ+GqW9hICIsZqvtDmGFFxddhU2ZiU1qe0c1CFn1YdO6m5qP5U7WAdlAC1QrOrY0I531SbGswiW1cZRRdUmaBwbSl64wvou/8b2qVn5vTB5AA/1GkEDwXRPhWstJImfKmftPHMxZ9v6KLDZd9S0A8p9Ui/WVao5VEU/C0g1DahQlnJdhXffLWJZ9sCLpuLMHJwPy5Wbe1GgLn3ljfop5D/5mAA0vdwlHHsBk7Tay07it1sEmoAo1oohxJlyDfi0amDtiIoltmTi/Ud6hX4ICPuNjWCb8ESV54aaSgQFM8kYnSQsr64BAmnf+O/b9VNf6ifK5+1/qvJ+Rh3tOeb6r+GQewE+lZpvDA9jYWKOorX9M55MDUCRevx99zY+pAcOpTsB+EP1geLCmkhj5jMyTNNL+K024nK+uLW5a/OcYc76EpsnXaWKaGKLeqIFELMLggf3LYLLUYcKPTeIW9uq59p3/M7TkNrGizMARTLum5UWKZx6mJJqM4i8ieMwvYyPMn1D8St7QrhxLQc7J1NOLyTv7NJrMXFxXkzfBKzO8NrsHfDbLq2uQTT7C8Cac+ciJdo5ohRc5sOOdmCZheiRfoza3q32EAIt7O2ztw+bNnXSkhX+vU1vWWGB5x6gFRmxMJw1jO9Snz4o1S5+skE3JJ7l/fyuMtk9h/QY2Rszvw4SkIAzgJTH4ebBtGo1Xi5oNRzTRAgA5UA49/gayMg+hs59BQ5DAgquBA+AxqAYOtji4Q9qTt4iVK3+qiY7Hh6Icp4FnrsX08U8haa+FxDUjPZYusTbsxfqpax76NlxD72GbvIEDOnyvqmk8A+vMD+5cc/9TEz02IQ/v2kiZ2PcBIuyMMCIEW1cheILquTIlriJNvls0Xv1skL8SAPmjHNm+Hkf8MScWr1VbeHknhb5fVV0FD/Hn8zgZehEAflRcsvaZINGJvqu7oAe7sTN+BPuYcWYlg47p1Dxc2oNjBcIbRXPvRx/dIy7QNmHZsRP80T2YXaxXVibWZVnPYzB5sOCxjvtG6e0gt/6vRf0b9hXzFgogR5Id22voIvt6QH8zts6uBhPYqY47Rz3+zwji2Mka/yHFa38oZmZ3ahYT/l35LU/+ArtaG2/AyIrLKewVGImnAjTsUPD6IOm44c3YSh1zHwk7Rc88lgUwCIDsf2EKCcwKYFnEdXhnRdN62LJ+/5zs+00jNsNM4ftpqXbGQHAnajlu/xc4KA0y2KqyZQAAAABJRU5ErkJggg==';
+ 
+/**
+ * Content-ID used to embed the logo inline in HTML emails.
+ * Templates reference it as `<img src="cid:intra-logo">`.
+ */
+export const LOGO_CID = 'intra-logo';
+ 
+export const LOGO_FILENAME = 'intra-logo.png';
+export const LOGO_CONTENT_TYPE = 'image/png';
+ 
+/**
+ * Decoded PNG bytes of the logo, suitable for use as an attachment
+ * `content` in nodemailer / @nestjs-modules/mailer.
+ */
+export const LOGO_BUFFER: Buffer = Buffer.from(
+    LOGO_DATA_URI.replace(/^data:image\/png;base64,/, ''),
+    'base64',
+);
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/index.html b/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/index.html new file mode 100644 index 00000000..82cd5c0c --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for organisation/application/ports + + + + + + + + + +
+
+

All files organisation/application/ports

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
position-hierarchy.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
position.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
team.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/position-hierarchy.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/position-hierarchy.repository.port.ts.html new file mode 100644 index 00000000..64fb0d2d --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/position-hierarchy.repository.port.ts.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for organisation/application/ports/position-hierarchy.repository.port.ts + + + + + + + + + +
+
+

All files / organisation/application/ports position-hierarchy.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23  +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { PositionHierarchyDomain } from '../../domain/position-hierarchy.domain';
+ 
+export const ORGANISATION_POSITION_HIERARCHY_REPOSITORY = Symbol(
+    'ORGANISATION.POSITION_HIERARCHY_REPOSITORY',
+);
+ 
+export interface PositionHierarchyRepositoryPort {
+    link(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<PositionHierarchyDomain>;
+    unlink(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<void>;
+    listSubordinates(
+        superiorPositionId: number,
+    ): Promise<PositionHierarchyDomain[]>;
+    listSuperiors(
+        subordinatePositionId: number,
+    ): Promise<PositionHierarchyDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/position.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/position.repository.port.ts.html new file mode 100644 index 00000000..6674da04 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/position.repository.port.ts.html @@ -0,0 +1,145 @@ + + + + + + Code coverage report for organisation/application/ports/position.repository.port.ts + + + + + + + + + +
+
+

All files / organisation/application/ports position.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21  +  +  +  +  +  +35x +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    PositionSearchQuery,
+    UpdatePositionPayload,
+} from '@intra/shared-kernel';
+import { PositionDomain } from '../../domain/position.domain';
+ 
+export const ORGANISATION_POSITION_REPOSITORY = Symbol(
+    'ORGANISATION.POSITION_REPOSITORY',
+);
+ 
+export interface PositionRepositoryPort {
+    create(position: PositionDomain): Promise<PositionDomain>;
+    findById(id: number): Promise<PositionDomain | null>;
+    search(query: PositionSearchQuery): Promise<PositionDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdatePositionPayload,
+    ): Promise<PositionDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/team.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/team.repository.port.ts.html new file mode 100644 index 00000000..dee67304 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/application/ports/team.repository.port.ts.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for organisation/application/ports/team.repository.port.ts + + + + + + + + + +
+
+

All files / organisation/application/ports team.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23  +  +  +  +25x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { TeamSearchQuery, UpdateTeamPayload } from '@intra/shared-kernel';
+import { TeamMembershipDomain } from '../../domain/team-membership.domain';
+import { TeamDomain } from '../../domain/team.domain';
+ 
+export const ORGANISATION_TEAM_REPOSITORY = Symbol(
+    'ORGANISATION.TEAM_REPOSITORY',
+);
+ 
+export interface TeamRepositoryPort {
+    create(team: TeamDomain): Promise<TeamDomain>;
+    findById(id: number): Promise<TeamDomain | null>;
+    search(query: TeamSearchQuery): Promise<TeamDomain[]>;
+    updateById(id: number, patch: UpdateTeamPayload): Promise<TeamDomain>;
+    deleteById(id: number): Promise<void>;
+    addMember(
+        teamId: number,
+        memberId: number,
+        isPrimary?: boolean | null,
+    ): Promise<TeamMembershipDomain>;
+    removeMember(teamId: number, memberId: number): Promise<void>;
+    listMembers(teamId: number): Promise<TeamMembershipDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/application/services/index.html b/apps/api/test/integration/coverage/lcov-report/organisation/application/services/index.html new file mode 100644 index 00000000..d6887de9 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/application/services/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for organisation/application/services + + + + + + + + + +
+
+

All files organisation/application/services

+
+ +
+ 98.97% + Statements + 97/98 +
+ + +
+ 84.09% + Branches + 37/44 +
+ + +
+ 100% + Functions + 25/25 +
+ + +
+ 98.86% + Lines + 87/88 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
position-hierarchy.service.ts +
+
100%24/2483.33%5/6100%7/7100%22/22
position.service.ts +
+
100%28/2875%9/12100%8/8100%24/24
team.service.ts +
+
97.82%45/4688.46%23/26100%10/1097.61%41/42
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/application/services/position-hierarchy.service.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/application/services/position-hierarchy.service.ts.html new file mode 100644 index 00000000..53f48496 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/application/services/position-hierarchy.service.ts.html @@ -0,0 +1,286 @@ + + + + + + Code coverage report for organisation/application/services/position-hierarchy.service.ts + + + + + + + + + +
+
+

All files / organisation/application/services position-hierarchy.service.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 83.33% + Branches + 5/6 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 22/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +686x +  +  +  +6x +6x +  +  +6x +  +  +6x +6x +  +  +  +  +  +  +11x +1x +  +  +  +  +10x +9x +  +8x +  +  +  +  +  +  +2x +2x +2x +  +  +  +  +  +2x +  +2x +2x +  +2x +  +  +  +  +  +  +  +2x +2x +  +  +2x +  +2x +  +  +  +  + 
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { PositionHierarchyDomain } from '../../domain/position-hierarchy.domain';
+import { PositionDomain } from '../../domain/position.domain';
+import type { PositionHierarchyRepositoryPort } from '../ports/position-hierarchy.repository.port';
+import { ORGANISATION_POSITION_HIERARCHY_REPOSITORY } from '../ports/position-hierarchy.repository.port';
+import { PositionService } from './position.service';
+ 
+@Injectable()
+export class PositionHierarchyService {
+    constructor(
+        @Inject(ORGANISATION_POSITION_HIERARCHY_REPOSITORY)
+        private readonly hierarchy: PositionHierarchyRepositoryPort,
+        private readonly positions: PositionService,
+    ) {}
+ 
+    async link(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<PositionHierarchyDomain> {
+        if (superiorPositionId === subordinatePositionId) {
+            throw new BadRequestException(
+                'Position cannot be a subordinate of itself',
+            );
+        }
+ 
+        await this.positions.getById(superiorPositionId);
+        await this.positions.getById(subordinatePositionId);
+ 
+        return this.hierarchy.link(superiorPositionId, subordinatePositionId);
+    }
+ 
+    async unlink(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<void> {
+        await this.positions.getById(superiorPositionId);
+        await this.positions.getById(subordinatePositionId);
+        await this.hierarchy.unlink(superiorPositionId, subordinatePositionId);
+    }
+ 
+    async listSubordinates(
+        superiorPositionId: number,
+    ): Promise<PositionDomain[]> {
+        await this.positions.getById(superiorPositionId);
+        const relations =
+            await this.hierarchy.listSubordinates(superiorPositionId);
+        return Promise.all(
+            relations.map((rel) =>
+                this.positions.getById(rel.subordinatePositionId),
+            ),
+        );
+    }
+ 
+    async listSuperiors(
+        subordinatePositionId: number,
+    ): Promise<PositionDomain[]> {
+        await this.positions.getById(subordinatePositionId);
+        const relations = await this.hierarchy.listSuperiors(
+            subordinatePositionId,
+        );
+        return Promise.all(
+            relations.map((rel) =>
+                this.positions.getById(rel.superiorPositionId),
+            ),
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/application/services/position.service.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/application/services/position.service.ts.html new file mode 100644 index 00000000..209373ec --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/application/services/position.service.ts.html @@ -0,0 +1,337 @@ + + + + + + Code coverage report for organisation/application/services/position.service.ts + + + + + + + + + +
+
+

All files / organisation/application/services position.service.ts

+
+ +
+ 100% + Statements + 28/28 +
+ + +
+ 75% + Branches + 9/12 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 24/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85  +  +  +  +  +50x +  +  +  +  +  +50x +  +  +  +50x +  +50x +  +  +50x +  +  +50x +  +50x +  +  +  +108x +  +  +  +108x +  +  +  +2x +  +  +  +53x +53x +46x +  +  +  +  +  +  +2x +1x +  +  +  +  +  +1x +  +  +  +3x +2x +2x +1x +  +  +  +  +  +  +  +  +  +1x +  +  +  +4x +  +4x +4x +  +  + 
import {
+    CreatePositionPayload,
+    PositionSearchQuery,
+    UpdatePositionPayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import {
+    POSITION_COMPETENCE_RELATION_REPOSITORY,
+    PositionCompetenceRelationRepositoryPort,
+} from 'src/contexts/library/application/ports/position-competence-relation.repository.port';
+import { PositionDomain } from '../../domain/position.domain';
+import type { PositionRepositoryPort } from '../ports/position.repository.port';
+import { ORGANISATION_POSITION_REPOSITORY } from '../ports/position.repository.port';
+ 
+@Injectable()
+export class PositionService {
+    constructor(
+        @Inject(ORGANISATION_POSITION_REPOSITORY)
+        private readonly positions: PositionRepositoryPort,
+        @Inject(POSITION_COMPETENCE_RELATION_REPOSITORY)
+        private readonly positionCompetenceRelations: PositionCompetenceRelationRepositoryPort,
+    ) {}
+ 
+    async create(payload: CreatePositionPayload): Promise<PositionDomain> {
+        const position = PositionDomain.create({
+            title: payload.title,
+            description: payload.description ?? null,
+        });
+        return this.positions.create(position);
+    }
+ 
+    async search(query: PositionSearchQuery): Promise<PositionDomain[]> {
+        return this.positions.search(query);
+    }
+ 
+    async getById(id: number): Promise<PositionDomain> {
+        const found = await this.positions.findById(id);
+        if (!found) throw new NotFoundException('Position not found');
+        return found;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdatePositionPayload,
+    ): Promise<PositionDomain> {
+        await this.getById(id);
+        const payload: UpdatePositionPayload = {
+            ...patch,
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+        };
+        return this.positions.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        const competences = await this.listCompetences(id);
+        if (competences.length > 0) {
+            throw new BadRequestException(
+                'Position #' +
+                    id +
+                    ' cannot be deleted. It has ' +
+                    competences.length +
+                    ' competences assigned to it.',
+            );
+        }
+        // await this.positionCompetenceRelations.deleteAllForPosition(id);
+        // await this.positionQuestionTemplateRelations.deleteAllForPosition(id);
+        await this.positions.deleteById(id);
+    }
+ 
+    async listCompetences(positionId: number): Promise<number[]> {
+        await this.getById(positionId);
+        const relations =
+            await this.positionCompetenceRelations.listByPosition(positionId);
+        return relations.map((r) => r.competenceId);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/application/services/team.service.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/application/services/team.service.ts.html new file mode 100644 index 00000000..60cdbd4d --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/application/services/team.service.ts.html @@ -0,0 +1,475 @@ + + + + + + Code coverage report for organisation/application/services/team.service.ts + + + + + + + + + +
+
+

All files / organisation/application/services team.service.ts

+
+ +
+ 97.82% + Statements + 45/46 +
+ + +
+ 88.46% + Branches + 23/26 +
+ + +
+ 100% + Functions + 10/10 +
+ + +
+ 97.61% + Lines + 41/42 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131  +  +  +  +  +  +6x +  +  +  +  +  +6x +  +6x +  +6x +  +  +6x +  +  +6x +6x +  +  +  +20x +2x +  +  +19x +  +  +  +  +19x +  +  +  +2x +  +  +  +23x +23x +19x +  +  +  +3x +  +2x +2x +  +  +1x +  +  +  +  +  +  +1x +  +  +  +3x +2x +2x +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +10x +9x +  +8x +  +  +  +  +8x +1x +  +7x +  +  +  +1x +1x +  +  +  +  +  +  +4x +4x +  +4x +  +1x +  +2x +2x +  +  +2x +  +  +  +  +  +  +1x +  +  + 
import {
+    AddTeamMemberPayload,
+    CreateTeamPayload,
+    TeamSearchQuery,
+    UpdateTeamPayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service';
+import { TeamMembershipDomain } from '../../domain/team-membership.domain';
+import { TeamDomain } from '../../domain/team.domain';
+import type { TeamRepositoryPort } from '../ports/team.repository.port';
+import { ORGANISATION_TEAM_REPOSITORY } from '../ports/team.repository.port';
+ 
+@Injectable()
+export class TeamService {
+    constructor(
+        @Inject(ORGANISATION_TEAM_REPOSITORY)
+        private readonly teams: TeamRepositoryPort,
+        private readonly identityUsers: IdentityUserService,
+    ) {}
+ 
+    async create(payload: CreateTeamPayload): Promise<TeamDomain> {
+        if (payload.headId !== undefined && payload.headId !== null) {
+            await this.identityUsers.getById(payload.headId);
+        }
+ 
+        const team = TeamDomain.create({
+            title: payload.title,
+            description: payload.description ?? null,
+            headId: payload.headId ?? null,
+        });
+        return this.teams.create(team);
+    }
+ 
+    async search(query: TeamSearchQuery): Promise<TeamDomain[]> {
+        return this.teams.search(query);
+    }
+ 
+    async getById(id: number): Promise<TeamDomain> {
+        const team = await this.teams.findById(id);
+        if (!team) throw new NotFoundException('Team not found');
+        return team;
+    }
+ 
+    async update(id: number, patch: UpdateTeamPayload): Promise<TeamDomain> {
+        await this.getById(id);
+ 
+        Eif (patch.headId !== undefined && patch.headId !== null) {
+            await this.identityUsers.getById(patch.headId);
+        }
+ 
+        const payload: UpdateTeamPayload = {
+            ...patch,
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+        };
+ 
+        return this.teams.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        const members = await this.listMembers(id);
+        if (members.length > 0) {
+            throw new BadRequestException(
+                'Team #' +
+                    id +
+                    ' cannot be deleted. It has ' +
+                    members.length +
+                    ' members assigned to it.',
+            );
+        }
+        await this.teams.deleteById(id);
+    }
+ 
+    async addMember(
+        teamId: number,
+        payload: AddTeamMemberPayload,
+        opts?: { withUser?: boolean },
+    ): Promise<TeamMembershipDomain> {
+        await this.getById(teamId);
+        const user = await this.identityUsers.getById(payload.memberId);
+ 
+        const membership = await this.teams.addMember(
+            teamId,
+            payload.memberId,
+            payload.isPrimary,
+        );
+        if (opts?.withUser) {
+            return membership.withUser(user);
+        }
+        return membership;
+    }
+ 
+    async removeMember(teamId: number, memberId: number): Promise<void> {
+        await this.getById(teamId);
+        await this.teams.removeMember(teamId, memberId);
+    }
+ 
+    async listMembers(
+        teamId: number,
+        opts?: { withUsers?: boolean },
+    ): Promise<TeamMembershipDomain[]> {
+        await this.getById(teamId);
+        const memberships = await this.teams.listMembers(teamId);
+ 
+        if (!opts?.withUsers) return memberships;
+ 
+        const withUsers = await Promise.all(
+            memberships.map(async (membership) => {
+                try {
+                    const user = await this.identityUsers.getById(
+                        membership.memberId,
+                    );
+                    return membership.withUser(user);
+                } catch {
+                    return membership;
+                }
+            }),
+        );
+ 
+        return withUsers;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/index.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/index.html new file mode 100644 index 00000000..6cc59e3c --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/index.html @@ -0,0 +1,176 @@ + + + + + + Code coverage report for organisation/infrastructure/mappers + + + + + + + + + +
+
+

All files organisation/infrastructure/mappers

+
+ +
+ 60.86% + Statements + 14/23 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 60% + Functions + 6/10 +
+ + +
+ 60.86% + Lines + 14/23 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
organisation.mapper.ts +
+
0%0/9100%0/00%0/40%0/9
position-hierarchy.mapper.ts +
+
100%3/3100%0/0100%1/1100%3/3
position.mapper.ts +
+
100%4/4100%0/0100%2/2100%4/4
team-membership.mapper.ts +
+
100%3/3100%0/0100%1/1100%3/3
team.mapper.ts +
+
100%4/4100%0/0100%2/2100%4/4
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/organisation.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/organisation.mapper.ts.html new file mode 100644 index 00000000..594992e4 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/organisation.mapper.ts.html @@ -0,0 +1,253 @@ + + + + + + Code coverage report for organisation/infrastructure/mappers/organisation.mapper.ts + + + + + + + + + +
+
+

All files / organisation/infrastructure/mappers organisation.mapper.ts

+
+ +
+ 0% + Statements + 0/9 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/4 +
+ + +
+ 0% + Lines + 0/9 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    Position,
+    PositionHierarchy,
+    Team,
+    TeamMembership,
+} from '@intra/database';
+import { PositionHierarchyDomain } from '../../domain/position-hierarchy.domain';
+import { PositionDomain } from '../../domain/position.domain';
+import { TeamMembershipDomain } from '../../domain/team-membership.domain';
+import { TeamDomain } from '../../domain/team.domain';
+ 
+export class OrganisationMapper {
+    static toTeamDomain(team: Team): TeamDomain {
+        return TeamDomain.create({
+            id: team.id,
+            title: team.title,
+            description: team.description,
+            headId: team.headId,
+            createdAt: team.createdAt,
+            updatedAt: team.updatedAt,
+        });
+    }
+ 
+    static toTeamMembershipDomain(
+        membership: TeamMembership,
+    ): TeamMembershipDomain {
+        return TeamMembershipDomain.create({
+            id: membership.id,
+            teamId: membership.teamId,
+            memberId: membership.memberId,
+            isPrimary: membership.isPrimary,
+            createdAt: membership.createdAt,
+        });
+    }
+ 
+    static toPositionDomain(position: Position): PositionDomain {
+        return PositionDomain.create({
+            id: position.id,
+            title: position.title,
+            description: position.description,
+            createdAt: position.createdAt,
+            updatedAt: position.updatedAt,
+        });
+    }
+ 
+    static toPositionHierarchyDomain(
+        relation: PositionHierarchy,
+    ): PositionHierarchyDomain {
+        return PositionHierarchyDomain.create({
+            id: relation.id,
+            superiorPositionId: relation.superiorPositionId,
+            subordinatePositionId: relation.subordinatePositionId,
+            createdAt: relation.createdAt,
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/position-hierarchy.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/position-hierarchy.mapper.ts.html new file mode 100644 index 00000000..0b6f63f4 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/position-hierarchy.mapper.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for organisation/infrastructure/mappers/position-hierarchy.mapper.ts + + + + + + + + + +
+
+

All files / organisation/infrastructure/mappers position-hierarchy.mapper.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +6x +  +6x +  +20x +  +  +  +  +  +  +  + 
import { PositionHierarchy } from '@intra/database';
+import { PositionHierarchyDomain } from '../../domain/position-hierarchy.domain';
+ 
+export class PositionHierarchyMapper {
+    static toDomain(relation: PositionHierarchy): PositionHierarchyDomain {
+        return PositionHierarchyDomain.create({
+            id: relation.id,
+            superiorPositionId: relation.superiorPositionId,
+            subordinatePositionId: relation.subordinatePositionId,
+            createdAt: relation.createdAt,
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/position.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/position.mapper.ts.html new file mode 100644 index 00000000..280de4b7 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/position.mapper.ts.html @@ -0,0 +1,154 @@ + + + + + + Code coverage report for organisation/infrastructure/mappers/position.mapper.ts + + + + + + + + + +
+
+

All files / organisation/infrastructure/mappers position.mapper.ts

+
+ +
+ 100% + Statements + 4/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 4/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24  +35x +  +35x +  +284x +  +  +  +  +  +  +  +  +  +  +  +222x +  +  +  +  +  + 
import { Position, Prisma } from '@intra/database';
+import { PositionDomain } from '../../domain/position.domain';
+ 
+export class PositionMapper {
+    static toDomain(position: Position): PositionDomain {
+        return PositionDomain.create({
+            id: position.id,
+            title: position.title,
+            description: position.description,
+            createdAt: position.createdAt,
+            updatedAt: position.updatedAt,
+        });
+    }
+ 
+    static toPrisma(
+        position: PositionDomain,
+    ): Prisma.PositionUncheckedCreateInput {
+        return {
+            title: position.title,
+            description: position.description,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/team-membership.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/team-membership.mapper.ts.html new file mode 100644 index 00000000..68645f8e --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/team-membership.mapper.ts.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for organisation/infrastructure/mappers/team-membership.mapper.ts + + + + + + + + + +
+
+

All files / organisation/infrastructure/mappers team-membership.mapper.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +25x +  +25x +  +22x +  +  +  +  +  +  +  +  + 
import { TeamMembership } from '@intra/database';
+import { TeamMembershipDomain } from '../../domain/team-membership.domain';
+ 
+export class TeamMembershipMapper {
+    static toDomain(membership: TeamMembership): TeamMembershipDomain {
+        return TeamMembershipDomain.create({
+            id: membership.id,
+            teamId: membership.teamId,
+            memberId: membership.memberId,
+            isPrimary: membership.isPrimary,
+            createdAt: membership.createdAt,
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/team.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/team.mapper.ts.html new file mode 100644 index 00000000..d45d9cef --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/mappers/team.mapper.ts.html @@ -0,0 +1,154 @@ + + + + + + Code coverage report for organisation/infrastructure/mappers/team.mapper.ts + + + + + + + + + +
+
+

All files / organisation/infrastructure/mappers team.mapper.ts

+
+ +
+ 100% + Statements + 4/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 4/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24  +25x +  +25x +  +74x +  +  +  +  +  +  +  +  +  +  +40x +  +  +  +  +  +  + 
import { Prisma, Team } from '@intra/database';
+import { TeamDomain } from '../../domain/team.domain';
+ 
+export class TeamMapper {
+    static toDomain(team: Team): TeamDomain {
+        return TeamDomain.create({
+            id: team.id,
+            title: team.title,
+            description: team.description,
+            headId: team.headId,
+            createdAt: team.createdAt,
+            updatedAt: team.updatedAt,
+        });
+    }
+ 
+    static toPrisma(team: TeamDomain): Prisma.TeamUncheckedCreateInput {
+        return {
+            title: team.title,
+            description: team.description,
+            headId: team.headId,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/index.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/index.html new file mode 100644 index 00000000..6bf42448 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for organisation/infrastructure/prisma-repositories + + + + + + + + + +
+
+

All files organisation/infrastructure/prisma-repositories

+
+ +
+ 97.56% + Statements + 80/82 +
+ + +
+ 77.77% + Branches + 42/54 +
+ + +
+ 100% + Functions + 24/24 +
+ + +
+ 97.29% + Lines + 72/74 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
position-hierarchy.repository.ts +
+
95.23%20/2170%7/10100%5/594.44%17/18
position.repository.ts +
+
100%24/2481.25%13/16100%8/8100%22/22
team.repository.ts +
+
97.29%36/3778.57%22/28100%11/1197.05%33/34
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/position-hierarchy.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/position-hierarchy.repository.ts.html new file mode 100644 index 00000000..fc693ecd --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/position-hierarchy.repository.ts.html @@ -0,0 +1,295 @@ + + + + + + Code coverage report for organisation/infrastructure/prisma-repositories/position-hierarchy.repository.ts + + + + + + + + + +
+
+

All files / organisation/infrastructure/prisma-repositories position-hierarchy.repository.ts

+
+ +
+ 95.23% + Statements + 20/21 +
+ + +
+ 70% + Branches + 7/10 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 94.44% + Lines + 17/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +716x +6x +6x +  +  +6x +  +  +6x +6x +  +  +  +  +  +14x +14x +  +  +12x +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +4x +  +  +  +4x +  +  +  +  +  +4x +  +  +  +4x +  +  + 
import { Prisma } from '@intra/database';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { PositionHierarchyRepositoryPort } from '../../application/ports/position-hierarchy.repository.port';
+import type { PositionHierarchyDomain } from '../../domain/position-hierarchy.domain';
+import { PositionHierarchyMapper } from '../mappers/position-hierarchy.mapper';
+ 
+@Injectable()
+export class PositionHierarchyRepository implements PositionHierarchyRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async link(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<PositionHierarchyDomain> {
+        try {
+            const created = await this.prisma.positionHierarchy.create({
+                data: { superiorPositionId, subordinatePositionId },
+            });
+            return PositionHierarchyMapper.toDomain(created);
+        } catch (error) {
+            Eif (
+                error instanceof Prisma.PrismaClientKnownRequestError &&
+                error.code === 'P2002'
+            ) {
+                const existing = await this.prisma.positionHierarchy.findUnique(
+                    {
+                        where: {
+                            superiorPositionId_subordinatePositionId: {
+                                superiorPositionId,
+                                subordinatePositionId,
+                            },
+                        },
+                    },
+                );
+                Eif (existing) return PositionHierarchyMapper.toDomain(existing);
+            }
+            throw error;
+        }
+    }
+ 
+    async unlink(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<void> {
+        await this.prisma.positionHierarchy.deleteMany({
+            where: { superiorPositionId, subordinatePositionId },
+        });
+    }
+ 
+    async listSubordinates(
+        superiorPositionId: number,
+    ): Promise<PositionHierarchyDomain[]> {
+        const relations = await this.prisma.positionHierarchy.findMany({
+            where: { superiorPositionId },
+            orderBy: { createdAt: 'desc' },
+        });
+        return relations.map(PositionHierarchyMapper.toDomain);
+    }
+ 
+    async listSuperiors(
+        subordinatePositionId: number,
+    ): Promise<PositionHierarchyDomain[]> {
+        const relations = await this.prisma.positionHierarchy.findMany({
+            where: { subordinatePositionId },
+            orderBy: { createdAt: 'desc' },
+        });
+        return relations.map(PositionHierarchyMapper.toDomain);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/position.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/position.repository.ts.html new file mode 100644 index 00000000..14548b8b --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/position.repository.ts.html @@ -0,0 +1,367 @@ + + + + + + Code coverage report for organisation/infrastructure/prisma-repositories/position.repository.ts + + + + + + + + + +
+
+

All files / organisation/infrastructure/prisma-repositories position.repository.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 81.25% + Branches + 13/16 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 22/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95  +53x +  +  +  +  +  +53x +53x +  +  +53x +  +  +53x +53x +  +  +365x +  +  +365x +  +  +  +55x +55x +  +  +  +7x +7x +  +7x +  +  +  +  +7x +  +  +  +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +7x +7x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +7x +7x +7x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    PositionSearchQuery,
+    PositionSortField,
+    SortDirection,
+    UpdatePositionPayload,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { PositionRepositoryPort } from '../../application/ports/position.repository.port';
+import { PositionDomain } from '../../domain/position.domain';
+import { PositionMapper } from '../mappers/position.mapper';
+ 
+@Injectable()
+export class PositionRepository implements PositionRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(position: PositionDomain): Promise<PositionDomain> {
+        const created = await this.prisma.position.create({
+            data: PositionMapper.toPrisma(position),
+        });
+        return PositionMapper.toDomain(created);
+    }
+ 
+    async findById(id: number): Promise<PositionDomain | null> {
+        const found = await this.prisma.position.findUnique({ where: { id } });
+        return found ? PositionMapper.toDomain(found) : null;
+    }
+ 
+    async search(query: PositionSearchQuery): Promise<PositionDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+ 
+        const items = await this.prisma.position.findMany({
+            where,
+            orderBy,
+        });
+ 
+        return items.map(PositionMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdatePositionPayload,
+    ): Promise<PositionDomain> {
+        const updated = await this.prisma.position.update({
+            where: { id },
+            data: patch,
+        });
+        return PositionMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.position.delete({ where: { id } });
+    }
+ 
+    private buildWhere(query: PositionSearchQuery): Prisma.PositionWhereInput {
+        const { search, title, description } = query;
+        return {
+            ...(search
+                ? {
+                      OR: [
+                          { title: { contains: search, mode: 'insensitive' } },
+                          {
+                              description: {
+                                  contains: search,
+                                  mode: 'insensitive',
+                              },
+                          },
+                      ],
+                  }
+                : {}),
+            ...(title
+                ? { title: { contains: title, mode: 'insensitive' } }
+                : {}),
+            ...(description
+                ? {
+                      description: {
+                          contains: description,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: PositionSearchQuery,
+    ): Prisma.PositionOrderByWithRelationInput[] {
+        const field = query.sortBy ?? PositionSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/team.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/team.repository.ts.html new file mode 100644 index 00000000..f8e56c99 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/organisation/infrastructure/prisma-repositories/team.repository.ts.html @@ -0,0 +1,511 @@ + + + + + + Code coverage report for organisation/infrastructure/prisma-repositories/team.repository.ts + + + + + + + + + +
+
+

All files / organisation/infrastructure/prisma-repositories team.repository.ts

+
+ +
+ 97.29% + Statements + 36/37 +
+ + +
+ 78.57% + Branches + 22/28 +
+ + +
+ 100% + Functions + 11/11 +
+ + +
+ 97.05% + Lines + 33/34 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +14341x +41x +  +  +  +  +  +41x +41x +  +  +  +41x +41x +  +  +41x +41x +  +  +40x +  +  +40x +  +  +  +25x +25x +  +  +  +5x +5x +  +5x +  +5x +  +  +  +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +15x +15x +  +  +  +  +  +  +14x +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +2x +  +  +  +  +  +5x +  +  +  +5x +  +  +  +5x +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +5x +5x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    SortDirection,
+    TeamSearchQuery,
+    TeamSortField,
+    UpdateTeamPayload,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import { TeamRepositoryPort } from '../../application/ports/team.repository.port';
+import type { TeamMembershipDomain } from '../../domain/team-membership.domain';
+import type { TeamDomain } from '../../domain/team.domain';
+import { TeamMembershipMapper } from '../mappers/team-membership.mapper';
+import { TeamMapper } from '../mappers/team.mapper';
+ 
+@Injectable()
+export class TeamRepository implements TeamRepositoryPort {
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(team: TeamDomain): Promise<TeamDomain> {
+        const created = await this.prisma.team.create({
+            data: TeamMapper.toPrisma(team),
+        });
+        return TeamMapper.toDomain(created);
+    }
+ 
+    async findById(id: number): Promise<TeamDomain | null> {
+        const found = await this.prisma.team.findUnique({ where: { id } });
+        return found ? TeamMapper.toDomain(found) : null;
+    }
+ 
+    async search(query: TeamSearchQuery): Promise<TeamDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+ 
+        const items = await this.prisma.team.findMany({ where, orderBy });
+ 
+        return items.map(TeamMapper.toDomain);
+    }
+ 
+    async updateById(
+        id: number,
+        patch: UpdateTeamPayload,
+    ): Promise<TeamDomain> {
+        const updated = await this.prisma.team.update({
+            where: { id },
+            data: patch,
+        });
+        return TeamMapper.toDomain(updated);
+    }
+ 
+    async deleteById(id: number): Promise<void> {
+        await this.prisma.team.delete({ where: { id } });
+    }
+ 
+    async addMember(
+        teamId: number,
+        memberId: number,
+        isPrimary?: boolean | null,
+    ): Promise<TeamMembershipDomain> {
+        try {
+            const created = await this.prisma.teamMembership.create({
+                data: {
+                    teamId,
+                    memberId: memberId,
+                    isPrimary: isPrimary ?? false,
+                },
+            });
+            return TeamMembershipMapper.toDomain(created);
+        } catch (error) {
+            Eif (
+                error instanceof Prisma.PrismaClientKnownRequestError &&
+                error.code === 'P2002'
+            ) {
+                const existing = await this.prisma.teamMembership.findUnique({
+                    where: {
+                        teamId_memberId_isPrimary: {
+                            teamId,
+                            memberId: memberId,
+                            isPrimary: isPrimary ?? false,
+                        },
+                    },
+                });
+                Eif (existing) return TeamMembershipMapper.toDomain(existing);
+            }
+            throw error;
+        }
+    }
+ 
+    async removeMember(teamId: number, memberId: number): Promise<void> {
+        await this.prisma.teamMembership.deleteMany({
+            where: { teamId, memberId: memberId },
+        });
+    }
+ 
+    async listMembers(teamId: number): Promise<TeamMembershipDomain[]> {
+        const memberships = await this.prisma.teamMembership.findMany({
+            where: { teamId },
+            orderBy: { createdAt: 'desc' },
+        });
+        return memberships.map(TeamMembershipMapper.toDomain);
+    }
+ 
+    private buildWhere(query: TeamSearchQuery): Prisma.TeamWhereInput {
+        const { search, headId, title, description } = query;
+        return {
+            ...(title
+                ? { title: { contains: title, mode: 'insensitive' } }
+                : {}),
+            ...(description
+                ? {
+                      description: {
+                          contains: description,
+                          mode: 'insensitive',
+                      },
+                  }
+                : {}),
+            ...(headId !== undefined ? { headId } : {}),
+            ...(search
+                ? {
+                      OR: [
+                          { title: { contains: search, mode: 'insensitive' } },
+                          {
+                              description: {
+                                  contains: search,
+                                  mode: 'insensitive',
+                              },
+                          },
+                      ],
+                  }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: TeamSearchQuery,
+    ): Prisma.TeamOrderByWithRelationInput[] {
+        const field = query.sortBy ?? TeamSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/prettify.css b/apps/api/test/integration/coverage/lcov-report/prettify.css new file mode 100644 index 00000000..b317a7cd --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/apps/api/test/integration/coverage/lcov-report/prettify.js b/apps/api/test/integration/coverage/lcov-report/prettify.js new file mode 100644 index 00000000..b3225238 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/cycle-stage.listener.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/cycle-stage.listener.ts.html new file mode 100644 index 00000000..d45f1827 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/cycle-stage.listener.ts.html @@ -0,0 +1,343 @@ + + + + + + Code coverage report for reporting/application/listeners/cycle-stage.listener.ts + + + + + + + + + +
+
+

All files / reporting/application/listeners cycle-stage.listener.ts

+
+ +
+ 86.2% + Statements + 25/29 +
+ + +
+ 66.66% + Branches + 16/24 +
+ + +
+ 66.66% + Functions + 2/3 +
+ + +
+ 85.18% + Lines + 23/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +8717x +17x +17x +17x +17x +17x +  +  +  +17x +  +  +  +  +  +  +  +17x +17x +  +  +17x +17x +  +17x +  +  +  +  +  +  +  +  +  +  +17x +  +  +2x +1x +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +1x +1x +  +  +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+import { Inject, Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service';
+import { CycleStageProcessedEvent } from '../../../feedback360/application/events/cycle-stage-processed.event';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../../../feedback360/application/ports/review.repository.port';
+import { CycleService } from '../../../feedback360/application/services/cycle.service';
+ 
+/**
+ * Event listener for cycle stage changes
+ * Automatically triggers strategic report generation
+ * when cycle reaches FINISHED stage
+ */
+@Injectable()
+export class CycleStageListener {
+    private readonly logger = new Logger(CycleStageListener.name);
+ 
+    constructor(
+        private readonly strategicReportingService: StrategicReportingService,
+        private readonly cycleService: CycleService,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+    ) {}
+ 
+    /*
+     * Handles the cycle.stage.processed event
+     * Triggers strategic report generation for the cycle
+     * when it transitions to the FINISHED stage
+     * updates review stage to PREPARING_REPORT
+     * and then moves it to PUBLISHED stage
+     */
+    @OnEvent('cycle.stage.processed')
+    async handleCycleStageProcessed(
+        event: CycleStageProcessedEvent,
+    ): Promise<void> {
+        if (event.currentStage !== CycleStage.FINISHED) {
+            return;
+        }
+ 
+        this.logger.debug(
+            `Cycle ${event.cycleId} with stage ${event.currentStage} was processed`,
+        );
+ 
+        setTimeout(async () => {
+            const reviews = await this.reviews.listByCycleId(event.cycleId);
+            if (reviews.length === 0) {
+                this.logger.debug(
+                    `No reviews found for cycle ${event.cycleId}. Skipping strategic report generation.`,
+                );
+            }
+        }, 10000);
+ 
+        this.logger.debug(
+            `Initiating strategic report generation for cycle ${event.cycleId}`,
+        );
+ 
+        try {
+            await this.cycleService.changeStage(
+                event.cycleId,
+                CycleStage.PREPARING_REPORT,
+            );
+ 
+            const report =
+                await this.strategicReportingService.generateStrategicReportForCycle(
+                    event.cycleId,
+                );
+ 
+            this.logger.debug(
+                `Successfully generated strategic report ${report.id} for cycle ${event.cycleId}`,
+            );
+ 
+            await this.cycleService.changeStage(
+                event.cycleId,
+                CycleStage.PUBLISHED,
+            );
+        } catch (error) {
+            this.logger.error(
+                `Failed to generate strategic report for cycle ${event.cycleId}: ${error.message}`,
+                error.stack,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/index.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/index.html new file mode 100644 index 00000000..391b8427 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for reporting/application/listeners + + + + + + + + + +
+
+

All files reporting/application/listeners

+
+ +
+ 90.38% + Statements + 47/52 +
+ + +
+ 71.42% + Branches + 30/42 +
+ + +
+ 80% + Functions + 4/5 +
+ + +
+ 89.58% + Lines + 43/48 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage.listener.ts +
+
86.2%25/2966.66%16/2466.66%2/385.18%23/27
review-stage.listener.ts +
+
95.65%22/2377.77%14/18100%2/295.23%20/21
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/review-stage.listener.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/review-stage.listener.ts.html new file mode 100644 index 00000000..9ba19524 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/listeners/review-stage.listener.ts.html @@ -0,0 +1,292 @@ + + + + + + Code coverage report for reporting/application/listeners/review-stage.listener.ts + + + + + + + + + +
+
+

All files / reporting/application/listeners review-stage.listener.ts

+
+ +
+ 95.65% + Statements + 22/23 +
+ + +
+ 77.77% + Branches + 14/18 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 95.23% + Lines + 20/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +7017x +17x +17x +17x +17x +17x +  +  +  +  +  +  +  +17x +17x +  +  +17x +17x +  +  +  +  +  +  +  +  +  +  +17x +  +  +2x +1x +  +  +1x +  +  +1x +  +  +  +1x +1x +  +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+import { Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { ReviewStageProcessedEvent } from 'src/contexts/feedback360/application/events/review-stage-processed.event';
+import { ReviewService } from '../../../feedback360/application/services/review.service';
+import { ReportingService } from '../services/reports.service';
+ 
+/**
+ * Event listener for review stage changes
+ * Automatically triggers individual report generation
+ * when review reaches FINISHED stage
+ */
+@Injectable()
+export class ReviewStageListener {
+    private readonly logger = new Logger(ReviewStageListener.name);
+ 
+    constructor(
+        private readonly reportService: ReportingService,
+        private readonly reviewService: ReviewService,
+    ) {}
+ 
+    /**
+     * Handles review.stage.processed events
+     * Triggers individual report generation for the review
+     * when review transitions to FINISHED stage
+     * updates review stage to PREPARING_REPORT
+     * and then moves it to PROCESSING_BY_HR stage
+     */
+    @OnEvent('review.stage.processed')
+    async handleReviewStageProcessed(
+        event: ReviewStageProcessedEvent,
+    ): Promise<void> {
+        if (event.currentStage !== ReviewStage.FINISHED) {
+            return;
+        }
+ 
+        this.logger.debug(
+            `Review ${event.reviewId} with stage ${event.currentStage} was processed`,
+        );
+        this.logger.debug(
+            `Initiating individual report generation for review ${event.reviewId}`,
+        );
+ 
+        try {
+            await this.reviewService.changeReviewStage(
+                event.reviewId,
+                ReviewStage.PREPARING_REPORT,
+            );
+ 
+            const report = await this.reportService.generateReportForReview(
+                event.reviewId,
+            );
+ 
+            this.logger.debug(
+                `Successfully generated individual report ${report.id} for review ${event.reviewId}`,
+            );
+ 
+            await this.reviewService.changeReviewStage(
+                event.reviewId,
+                ReviewStage.PROCESSING_BY_HR,
+            );
+        } catch (error) {
+            this.logger.error(
+                `Failed to generate individual report for review ${event.reviewId}: ${error.message}`,
+                error.stack,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/index.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/index.html new file mode 100644 index 00000000..0595c434 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/index.html @@ -0,0 +1,206 @@ + + + + + + Code coverage report for reporting/application/ports + + + + + + + + + +
+
+

All files reporting/application/ports

+
+ +
+ 100% + Statements + 7/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 7/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
report-analytics.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
report-comment.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
report-insight.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
report.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
strategic-report-analytics.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
strategic-report-insight.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
strategic-report.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-analytics.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-analytics.repository.port.ts.html new file mode 100644 index 00000000..229d7cc6 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-analytics.repository.port.ts.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for reporting/application/ports/report-analytics.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports report-analytics.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +  +7x +  +  +  +  +  +  +  +  +  +  +  + 
import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+ 
+export const REPORT_ANALYTICS_REPOSITORY = Symbol(
+    'REPORTING.REPORT_ANALYTICS_REPOSITORY',
+);
+ 
+export interface ReportAnalyticsRepositoryPort {
+    findByReportId(reportId: number): Promise<ReportAnalyticsDomain[]>;
+    findById(id: number): Promise<ReportAnalyticsDomain | null>;
+    createMany(
+        reportId: number,
+        analytics: ReportAnalyticsDomain[],
+    ): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-comment.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-comment.repository.port.ts.html new file mode 100644 index 00000000..1ee69b52 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-comment.repository.port.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for reporting/application/ports/report-comment.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports report-comment.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +7x +  +  +  +  +  +  +  +  + 
import { ReportCommentDomain } from '../../domain/report-comment.domain';
+ 
+export const REPORT_COMMENT_REPOSITORY = Symbol(
+    'REPORTING.REPORT_COMMENT_REPOSITORY',
+);
+ 
+export interface ReportCommentRepositoryPort {
+    findByReportId(reportId: number): Promise<ReportCommentDomain[]>;
+    findById(id: number): Promise<ReportCommentDomain | null>;
+    create(comment: ReportCommentDomain): Promise<ReportCommentDomain>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-insight.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-insight.repository.port.ts.html new file mode 100644 index 00000000..da42b1cf --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report-insight.repository.port.ts.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for reporting/application/ports/report-insight.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports report-insight.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +  +7x +  +  +  +  +  +  +  +  +  +  +  + 
import { ReportInsightDomain } from '../../domain/report-insight.domain';
+ 
+export const REPORT_INSIGHT_REPOSITORY = Symbol(
+    'REPORTING.REPORT_INSIGHT_REPOSITORY',
+);
+ 
+export interface ReportInsightRepositoryPort {
+    findByReportId(reportId: number): Promise<ReportInsightDomain[]>;
+    findById(id: number): Promise<ReportInsightDomain | null>;
+    createMany(
+        reportId: number,
+        insights: ReportInsightDomain[],
+    ): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report.repository.port.ts.html new file mode 100644 index 00000000..3a843389 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/report.repository.port.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for reporting/application/ports/report.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports report.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +7x +  +  +  +  +  +  +  + 
import { ReportSearchQuery } from '@intra/shared-kernel';
+import { ReportDomain } from '../../domain/report.domain';
+ 
+export const REPORT_REPOSITORY = Symbol('REPORTING.REPORT_REPOSITORY');
+ 
+export interface ReportRepositoryPort {
+    create(report: ReportDomain): Promise<ReportDomain>;
+    findById(id: number): Promise<ReportDomain | null>;
+    findByReviewId(reviewId: number): Promise<ReportDomain | null>;
+    search(query: ReportSearchQuery): Promise<ReportDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report-analytics.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report-analytics.repository.port.ts.html new file mode 100644 index 00000000..f0c0416d --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report-analytics.repository.port.ts.html @@ -0,0 +1,133 @@ + + + + + + Code coverage report for reporting/application/ports/strategic-report-analytics.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports strategic-report-analytics.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { StrategicReportAnalyticsDomain } from '../../domain/strategic-report-analytics.domain';
+ 
+export const STRATEGIC_REPORT_ANALYTICS_REPOSITORY = Symbol(
+    'REPORTING.STRATEGIC_REPORT_ANALYTICS_REPOSITORY',
+);
+ 
+export interface StrategicReportAnalyticsRepositoryPort {
+    findByStrategicReportId(
+        strategicReportId: number,
+    ): Promise<StrategicReportAnalyticsDomain[]>;
+    findById(id: number): Promise<StrategicReportAnalyticsDomain | null>;
+    createMany(
+        strategicReportId: number,
+        analytics: StrategicReportAnalyticsDomain[],
+    ): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report-insight.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report-insight.repository.port.ts.html new file mode 100644 index 00000000..40822709 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report-insight.repository.port.ts.html @@ -0,0 +1,133 @@ + + + + + + Code coverage report for reporting/application/ports/strategic-report-insight.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports strategic-report-insight.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { StrategicReportInsightDomain } from '../../domain/startegic-report-insight.domain';
+ 
+export const STRATEGIC_REPORT_INSIGHT_REPOSITORY = Symbol(
+    'REPORTING.STRATEGIC_REPORT_INSIGHT_REPOSITORY',
+);
+ 
+export interface StrategicReportInsightRepositoryPort {
+    findByStrategicReportId(
+        strategicReportId: number,
+    ): Promise<StrategicReportInsightDomain[]>;
+    findById(id: number): Promise<StrategicReportInsightDomain | null>;
+    createMany(
+        strategicReportId: number,
+        insights: StrategicReportInsightDomain[],
+    ): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report.repository.port.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report.repository.port.ts.html new file mode 100644 index 00000000..29bb188a --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/ports/strategic-report.repository.port.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for reporting/application/ports/strategic-report.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports strategic-report.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +  +19x +  +  +  +  +  +  +  +  +  + 
import { StrategicReportSearchQuery } from '@intra/shared-kernel';
+import { StrategicReportDomain } from '../../domain/strategic-report.domain';
+ 
+export const STRATEGIC_REPORT_REPOSITORY = Symbol(
+    'REPORTING.STRATEGIC_REPORT_REPOSITORY',
+);
+ 
+export interface StrategicReportRepositoryPort {
+    create(report: StrategicReportDomain): Promise<StrategicReportDomain>;
+    findById(id: number): Promise<StrategicReportDomain | null>;
+    findByCycleId(cycleId: number): Promise<StrategicReportDomain | null>;
+    search(query: StrategicReportSearchQuery): Promise<StrategicReportDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/index.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/index.html new file mode 100644 index 00000000..f88025d4 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/index.html @@ -0,0 +1,221 @@ + + + + + + Code coverage report for reporting/application/services + + + + + + + + + +
+
+

All files reporting/application/services

+
+ +
+ 86.93% + Statements + 639/735 +
+ + +
+ 63.53% + Branches + 413/650 +
+ + +
+ 85.1% + Functions + 120/141 +
+ + +
+ 88.31% + Lines + 612/693 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
report-analytics.service.ts +
+
94.44%17/1866.66%8/12100%3/393.75%15/16
report-comment.service.ts +
+
100%18/1878.57%11/14100%4/4100%16/16
report-insight.service.ts +
+
94.44%17/1866.66%8/12100%3/393.75%15/16
reports.service.ts +
+
84.7%277/32768.34%231/33885.96%49/5785.85%267/311
startegic-report-insight.service.ts +
+
94.44%17/1866.66%8/12100%3/393.75%15/16
strategic-report-analytics.service.ts +
+
94.44%17/1866.66%8/12100%3/393.75%15/16
strategic-reports.service.ts +
+
85.71%252/29453.87%125/23279.03%49/6288.29%249/282
text-answer.service.ts +
+
100%24/2477.77%14/18100%6/6100%20/20
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-analytics.service.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-analytics.service.ts.html new file mode 100644 index 00000000..b7abbfac --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-analytics.service.ts.html @@ -0,0 +1,238 @@ + + + + + + Code coverage report for reporting/application/services/report-analytics.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services report-analytics.service.ts

+
+ +
+ 94.44% + Statements + 17/18 +
+ + +
+ 66.66% + Branches + 8/12 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 93.75% + Lines + 15/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +5217x +  +  +17x +  +  +  +17x +  +  +17x +  +  +17x +17x +  +  +  +  +  +  +4x +  +2x +  +2x +  +  +  +  +  +2x +  +  +  +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+import {
+    REPORT_ANALYTICS_REPOSITORY,
+    ReportAnalyticsRepositoryPort,
+} from '../ports/report-analytics.repository.port';
+import { ReportingService } from './reports.service';
+ 
+@Injectable()
+export class ReportAnalyticsService {
+    constructor(
+        @Inject(REPORT_ANALYTICS_REPOSITORY)
+        private readonly analytics: ReportAnalyticsRepositoryPort,
+        private readonly reportingService: ReportingService,
+    ) {}
+ 
+    async getByReportId(
+        reportId: number,
+        actor?: UserDomain,
+    ): Promise<ReportAnalyticsDomain[]> {
+        await this.reportingService.getById(reportId, actor);
+ 
+        const analytics = await this.analytics.findByReportId(reportId);
+ 
+        Iif (!analytics) {
+            throw new NotFoundException(
+                `Report analytics with report id ${reportId} was not found`,
+            );
+        }
+ 
+        return analytics;
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<ReportAnalyticsDomain> {
+        const analytics = await this.analytics.findById(id);
+ 
+        if (!analytics) {
+            throw new NotFoundException(
+                `Report analytics with id ${id} was not found`,
+            );
+        }
+ 
+        await this.reportingService.getById(analytics.reportId, actor);
+ 
+        return analytics;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-comment.service.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-comment.service.ts.html new file mode 100644 index 00000000..7f60b15d --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-comment.service.ts.html @@ -0,0 +1,235 @@ + + + + + + Code coverage report for reporting/application/services/report-comment.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services report-comment.service.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 78.57% + Branches + 11/14 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +5117x +  +  +17x +  +  +  +17x +  +  +17x +  +  +17x +17x +  +  +  +  +  +  +3x +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +4x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { ReportCommentDomain } from '../../domain/report-comment.domain';
+import {
+    REPORT_COMMENT_REPOSITORY,
+    ReportCommentRepositoryPort,
+} from '../ports/report-comment.repository.port';
+import { ReportingService } from './reports.service';
+ 
+@Injectable()
+export class ReportCommentService {
+    constructor(
+        @Inject(REPORT_COMMENT_REPOSITORY)
+        private readonly comments: ReportCommentRepositoryPort,
+        private readonly reportingService: ReportingService,
+    ) {}
+ 
+    async getByReportId(
+        reportId: number,
+        actor?: UserDomain,
+    ): Promise<ReportCommentDomain[]> {
+        await this.reportingService.getById(reportId, actor);
+ 
+        const comments = await this.comments.findByReportId(reportId);
+ 
+        if (!comments || comments.length === 0) {
+            throw new NotFoundException(
+                `Report comments with report id ${reportId} were not found`,
+            );
+        }
+ 
+        return comments;
+    }
+ 
+    async getById(id: number): Promise<ReportCommentDomain> {
+        const comment = await this.comments.findById(id);
+ 
+        if (!comment) {
+            throw new NotFoundException(
+                `Report comment with id ${id} was not found`,
+            );
+        }
+ 
+        return comment;
+    }
+ 
+    async create(comment: ReportCommentDomain): Promise<ReportCommentDomain> {
+        return this.comments.create(comment);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-insight.service.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-insight.service.ts.html new file mode 100644 index 00000000..b774152c --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/report-insight.service.ts.html @@ -0,0 +1,238 @@ + + + + + + Code coverage report for reporting/application/services/report-insight.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services report-insight.service.ts

+
+ +
+ 94.44% + Statements + 17/18 +
+ + +
+ 66.66% + Branches + 8/12 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 93.75% + Lines + 15/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +5217x +  +  +17x +  +  +  +17x +  +  +17x +  +  +17x +17x +  +  +  +  +  +  +4x +  +2x +  +2x +  +  +  +  +  +2x +  +  +  +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { ReportInsightDomain } from '../../domain/report-insight.domain';
+import {
+    REPORT_INSIGHT_REPOSITORY,
+    ReportInsightRepositoryPort,
+} from '../ports/report-insight.repository.port';
+import { ReportingService } from './reports.service';
+ 
+@Injectable()
+export class ReportInsightService {
+    constructor(
+        @Inject(REPORT_INSIGHT_REPOSITORY)
+        private readonly insights: ReportInsightRepositoryPort,
+        private readonly reportingService: ReportingService,
+    ) {}
+ 
+    async getByReportId(
+        reportId: number,
+        actor?: UserDomain,
+    ): Promise<ReportInsightDomain[]> {
+        await this.reportingService.getById(reportId, actor);
+ 
+        const insights = await this.insights.findByReportId(reportId);
+ 
+        Iif (!insights) {
+            throw new NotFoundException(
+                `Report insights with report id ${reportId} was not found`,
+            );
+        }
+ 
+        return insights;
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<ReportInsightDomain> {
+        const insight = await this.insights.findById(id);
+ 
+        if (!insight) {
+            throw new NotFoundException(
+                `Report insight with id ${id} was not found`,
+            );
+        }
+ 
+        await this.reportingService.getById(insight.reportId, actor);
+ 
+        return insight;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/reports.service.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/reports.service.ts.html new file mode 100644 index 00000000..e6cba779 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/reports.service.ts.html @@ -0,0 +1,3958 @@ + + + + + + Code coverage report for reporting/application/services/reports.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services reports.service.ts

+
+ +
+ 84.7% + Statements + 277/327 +
+ + +
+ 68.34% + Branches + 231/338 +
+ + +
+ 85.96% + Functions + 49/57 +
+ + +
+ 85.85% + Lines + 267/311 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +1026 +1027 +1028 +1029 +1030 +1031 +1032 +1033 +1034 +1035 +1036 +1037 +1038 +1039 +1040 +1041 +1042 +1043 +1044 +1045 +1046 +1047 +1048 +1049 +1050 +1051 +1052 +1053 +1054 +1055 +1056 +1057 +1058 +1059 +1060 +1061 +1062 +1063 +1064 +1065 +1066 +1067 +1068 +1069 +1070 +1071 +1072 +1073 +1074 +1075 +1076 +1077 +1078 +1079 +1080 +1081 +1082 +1083 +1084 +1085 +1086 +1087 +1088 +1089 +1090 +1091 +1092 +1093 +1094 +1095 +1096 +1097 +1098 +1099 +1100 +1101 +1102 +1103 +1104 +1105 +1106 +1107 +1108 +1109 +1110 +1111 +1112 +1113 +1114 +1115 +1116 +1117 +1118 +1119 +1120 +1121 +1122 +1123 +1124 +1125 +1126 +1127 +1128 +1129 +1130 +1131 +1132 +1133 +1134 +1135 +1136 +1137 +1138 +1139 +1140 +1141 +1142 +1143 +1144 +1145 +1146 +1147 +1148 +1149 +1150 +1151 +1152 +1153 +1154 +1155 +1156 +1157 +1158 +1159 +1160 +1161 +1162 +1163 +1164 +1165 +1166 +1167 +1168 +1169 +1170 +1171 +1172 +1173 +1174 +1175 +1176 +1177 +1178 +1179 +1180 +1181 +1182 +1183 +1184 +1185 +1186 +1187 +1188 +1189 +1190 +1191 +1192 +1193 +1194 +1195 +1196 +1197 +1198 +1199 +1200 +1201 +1202 +1203 +1204 +1205 +1206 +1207 +1208 +1209 +1210 +1211 +1212 +1213 +1214 +1215 +1216 +1217 +1218 +1219 +1220 +1221 +1222 +1223 +1224 +1225 +1226 +1227 +1228 +1229 +1230 +1231 +1232 +1233 +1234 +1235 +1236 +1237 +1238 +1239 +1240 +1241 +1242 +1243 +1244 +1245 +1246 +1247 +1248 +1249 +1250 +1251 +1252 +1253 +1254 +1255 +1256 +1257 +1258 +1259 +1260 +1261 +1262 +1263 +1264 +1265 +1266 +1267 +1268 +1269 +1270 +1271 +1272 +1273 +1274 +1275 +1276 +1277 +1278 +1279 +1280 +1281 +1282 +1283 +1284 +1285 +1286 +1287 +1288 +1289 +1290 +1291 +129217x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +17x +  +  +  +  +  +  +17x +17x +  +  +  +17x +  +  +  +  +17x +  +  +  +17x +  +  +  +17x +  +  +  +17x +  +  +  +17x +  +  +  +  +17x +  +  +  +17x +  +  +  +  +17x +17x +17x +17x +  +  +  +17x +  +  +  +17x +  +  +  +17x +  +  +  +  +  +17x +17x +  +  +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +  +17x +  +  +  +  +  +3x +2x +  +  +  +  +  +  +  +3x +  +  +2x +  +  +2x +1x +  +  +  +  +  +  +17x +  +17x +4x +  +  +13x +  +10x +  +  +  +  +  +  +  +  +  +  +  +14x +  +  +14x +  +  +14x +  +4x +  +4x +  +  +  +  +4x +4x +  +4x +  +3x +  +  +  +3x +  +3x +  +  +  +  +  +  +  +  +2x +2x +1x +  +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +4x +  +4x +4x +1x +1x +  +  +3x +3x +1x +  +  +2x +  +  +  +2x +2x +  +  +2x +  +  +1x +  +  +  +  +  +2x +  +2x +2x +2x +  +  +2x +  +  +  +2x +  +2x +2x +  +  +  +  +2x +  +  +  +  +2x +  +  +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +  +2x +  +2x +  +2x +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +2x +  +  +  +  +2x +  +  +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +1x +  +  +  +  +1x +  +  +  +  +  +  +2x +2x +2x +  +  +  +2x +2x +2x +  +  +  +  +2x +2x +  +  +2x +1x +1x +  +1x +  +  +  +1x +1x +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +25x +13x +12x +25x +11x +12x +  +  +11x +  +  +  +  +  +  +  +  +  +  +41x +  +33x +33x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +4x +  +  +4x +2x +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +1x +  +  +2x +  +  +  +2x +2x +2x +2x +2x +  +2x +  +1x +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +1x +1x +  +1x +1x +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +1x +  +  +1x +1x +  +1x +1x +  +  +1x +  +  +  +2x +2x +2x +2x +  +2x +1x +  +  +1x +1x +  +1x +1x +  +  +1x +  +  +1x +1x +  +1x +1x +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +1x +  +1x +  +2x +2x +2x +  +  +2x +2x +2x +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +6x +  +  +  +2x +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +16x +16x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +1x +  +2x +1x +  +2x +1x +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +24x +14x +  +10x +  +  +  +  +  +  +  +  +48x +12x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +4x +4x +  +4x +  +  +  +4x +  +  +  +4x +  +  +  +  +4x +  +  +  +4x +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +2x +2x +4x +2x +2x +  +  +4x +  +4x +  +4x +  +4x +  +4x +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +17x +6x +12x +  +6x +4x +  +  +  +  +  +  +  +  +4x +4x +8x +  +4x +  +  + 
import {
+    AnswerType,
+    CompetenceAccumulator,
+    CYCLE_CONSTRAINTS,
+    EntityAverageInsightsDto,
+    EntityInsightSummaryDto,
+    EntitySummaryTotals,
+    EntityType,
+    IdentityRole,
+    InsightType,
+    REPORT_ANALYTICS_CONSTRAINTS,
+    ReportSearchQuery,
+    RESPONDENT_CATEGORIES,
+    RespondentCategory,
+} from '@intra/shared-kernel';
+import {
+    ForbiddenException,
+    Inject,
+    Injectable,
+    Logger,
+    NotFoundException,
+} from '@nestjs/common';
+import Decimal from 'decimal.js';
+import {
+    CYCLE_REPOSITORY,
+    CycleRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/cycle.repository.port';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/reviewer.repository.port';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../../../feedback360/application/ports/answer.repository.port';
+import {
+    CLUSTER_SCORE_REPOSITORY,
+    ClusterScoreRepositoryPort,
+} from '../../../feedback360/application/ports/cluster-score.repository.port';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../../../feedback360/application/ports/respondent.repository.port';
+import {
+    REVIEW_QUESTION_RELATION_REPOSITORY,
+    ReviewQuestionRelationRepositoryPort,
+} from '../../../feedback360/application/ports/review-question-relation.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../../../feedback360/application/ports/review.repository.port';
+import { AnswerDomain } from '../../../feedback360/domain/answer.domain';
+import { ClusterScoreDomain } from '../../../feedback360/domain/cluster-score.domain';
+import { RespondentDomain } from '../../../feedback360/domain/respondent.domain';
+import { ReviewQuestionRelationDomain } from '../../../feedback360/domain/review-question-relation.domain';
+import { ReviewDomain } from '../../../feedback360/domain/review.domain';
+import {
+    CLUSTER_REPOSITORY,
+    ClusterRepositoryPort,
+} from '../../../library/application/ports/cluster.repository.port';
+import { ClusterDomain } from '../../../library/domain/cluster.domain';
+import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+import { ReportInsightDomain } from '../../domain/report-insight.domain';
+import { ReportDomain } from '../../domain/report.domain';
+import {
+    REPORT_ANALYTICS_REPOSITORY,
+    ReportAnalyticsRepositoryPort,
+} from '../ports/report-analytics.repository.port';
+import {
+    REPORT_COMMENT_REPOSITORY,
+    ReportCommentRepositoryPort,
+} from '../ports/report-comment.repository.port';
+import {
+    REPORT_INSIGHT_REPOSITORY,
+    ReportInsightRepositoryPort,
+} from '../ports/report-insight.repository.port';
+import {
+    REPORT_REPOSITORY,
+    ReportRepositoryPort,
+} from '../ports/report.repository.port';
+ 
+@Injectable()
+export class ReportingService {
+    private readonly logger = new Logger(ReportingService.name);
+ 
+    constructor(
+        @Inject(REPORT_REPOSITORY)
+        private readonly reports: ReportRepositoryPort,
+        @Inject(REPORT_ANALYTICS_REPOSITORY)
+        private readonly analyticsRepo: ReportAnalyticsRepositoryPort,
+        @Inject(REPORT_INSIGHT_REPOSITORY)
+        private readonly insightsRepo: ReportInsightRepositoryPort,
+        @Inject(REPORT_COMMENT_REPOSITORY)
+        private readonly commentsRepo: ReportCommentRepositoryPort,
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        @Inject(REVIEWER_REPOSITORY)
+        private readonly reviewers: ReviewerRepositoryPort,
+        @Inject(ANSWER_REPOSITORY)
+        private readonly answers: AnswerRepositoryPort,
+        @Inject(REVIEW_QUESTION_RELATION_REPOSITORY)
+        private readonly reviewQuestionRelations: ReviewQuestionRelationRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        @Inject(CLUSTER_REPOSITORY)
+        private readonly clusters: ClusterRepositoryPort,
+        @Inject(CLUSTER_SCORE_REPOSITORY)
+        private readonly clusterScores: ClusterScoreRepositoryPort,
+        @Inject(CYCLE_REPOSITORY)
+        private readonly cyclesRepo: CycleRepositoryPort,
+    ) {}
+ 
+    private readonly isProduction = process.env.NODE_ENV === 'production';
+ 
+    async search(
+        query: ReportSearchQuery,
+        actor?: UserDomain,
+    ): Promise<ReportDomain[]> {
+        await this.checkAccessToAllReports(actor);
+        return await this.reports.search(query);
+    }
+ 
+    /**
+     * Checks if the actor has access to all reports (HR or Admin only).
+     * @param actor The actor to check access for.
+     */
+    async checkAccessToAllReports(actor?: UserDomain): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (!isAdminOrHr) {
+            throw new ForbiddenException(
+                'You do not have permission to view this report',
+            );
+        }
+    }
+ 
+    async getById(id: number, actor?: UserDomain): Promise<ReportDomain> {
+        const report = await this.reports.findById(id);
+ 
+        if (!report) {
+            throw new NotFoundException(`Report with id ${id} was not found`);
+        }
+ 
+        await this.checkAccessToReport(report, actor);
+ 
+        return report;
+    }
+ 
+    /**
+     * Checks if the actor has access to the report.
+     * @param report The report to check access for.
+     * @param actor The actor to check access for.
+     */
+    async checkAccessToReport(
+        report: ReportDomain,
+        actor?: UserDomain,
+    ): Promise<void> {
+        Iif (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const review = await this.reviews.findById(report.reviewId);
+ 
+        Iif (!review) {
+            throw new NotFoundException(`Review for report ${report.id} was not found. 
+                Unable to check access to report`);
+        }
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        Iif (reviewers.length > 0) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view this report',
+        );
+    }
+ 
+    async getByReviewId(
+        reviewId: number,
+        actor?: UserDomain,
+    ): Promise<ReportDomain> {
+        const report = await this.reports.findByReviewId(reviewId);
+        if (!report) {
+            throw new NotFoundException(
+                `Report for review ${reviewId} was not found`,
+            );
+        }
+ 
+        await this.checkAccessToReport(report, actor);
+ 
+        return report;
+    }
+ 
+    /**
+     * Generates a comprehensive report for a review if anonymity threshold is met.
+     * Calculates respondent counts and derives analytics from answers.
+     * @param reviewId The ID of the review to generate a report for.
+     * @returns The generated report.
+     */
+    async generateReportForReview(reviewId: number): Promise<ReportDomain> {
+        this.logger.debug(`Started report generation for review ${reviewId}`);
+ 
+        const existing = await this.reports.findByReviewId(reviewId);
+        if (existing) {
+            this.logger.debug(`Report already exists for review ${reviewId}`);
+            return existing;
+        }
+ 
+        const review = await this.reviews.findById(reviewId);
+        if (!review) {
+            throw new NotFoundException(`Review with id ${reviewId} not found`);
+        }
+ 
+        const allRespondents = await this.respondents.listByReview(
+            reviewId,
+            {},
+        );
+        const respondentCount = allRespondents.length;
+        const respondentCategories: RespondentCategory[] = [
+            ...new Set(
+                allRespondents
+                    .map((r) => r.category)
+                    .sort(
+                        (a, b) =>
+                            RESPONDENT_CATEGORIES.indexOf(a) -
+                            RESPONDENT_CATEGORIES.indexOf(b),
+                    ),
+            ),
+        ];
+        const answersCounts =
+            await this.answers.getAnswersCountByRespondentCategories(reviewId);
+ 
+        let answerCount = 0;
+        answersCounts.forEach((stat) => {
+            answerCount += stat.answers;
+        });
+ 
+        Iif (this.isProduction) {
+            await this.verifyAnonimityThreshold(review, answersCounts);
+        }
+ 
+        const actualAnswersByCategory = answersCounts.reduce(
+            (acc, stat) => {
+                acc[stat.respondentCategory] = stat.answers;
+                return acc;
+            },
+            {} as Record<string, number>,
+        );
+ 
+        const teamTurnout = this.calculateTurnout(
+            allRespondents,
+            actualAnswersByCategory[RespondentCategory.TEAM] || 0,
+            RespondentCategory.TEAM,
+        );
+        const otherTurnout = this.calculateTurnout(
+            allRespondents,
+            actualAnswersByCategory[RespondentCategory.OTHER] || 0,
+            RespondentCategory.OTHER,
+        );
+ 
+        this.logger.debug(
+            `Calculated respondent counts: ${respondentCount} total in ${respondentCategories.join(', ')} categories, answer count ${answerCount}, team turnout ${teamTurnout}, other turnout ${otherTurnout}`,
+        );
+ 
+        const answers = await this.answers.list({
+            reviewId,
+            answerType: AnswerType.NUMERICAL_SCALE,
+        });
+ 
+        const relations = await this.reviewQuestionRelations.listByReview(
+            reviewId,
+            {},
+        );
+ 
+        const maxScore = new Decimal(REPORT_ANALYTICS_CONSTRAINTS.SCORE.MAX);
+        const { questionAnalytics, competenceAnalytics, questionTotals } =
+            this.buildAnalyticsPayload(answers, relations, maxScore);
+ 
+        const competenceTotals = this.calculateCompetenceSummaryTotals(
+            competenceAnalytics,
+            maxScore,
+        );
+ 
+        const report = ReportDomain.create({
+            reviewId,
+            cycleId: review.cycleId,
+            respondentCount,
+            respondentCategories,
+            answerCount,
+            turnoutPctOfTeam: teamTurnout,
+            turnoutPctOfOther: otherTurnout,
+            questionTotAvgBySelf: questionTotals.averageBySelfAssessment,
+            questionTotAvgByTeam: questionTotals.averageByTeam,
+            questionTotAvgByOthers: questionTotals.averageByOther,
+            questionTotPctBySelf: questionTotals.percentageBySelfAssessment,
+            questionTotPctByTeam: questionTotals.percentageByTeam,
+            questionTotPctByOthers: questionTotals.percentageByOther,
+            questionTotDeltaPctByTeam: questionTotals.deltaPercentageByTeam,
+            questionTotDeltaPctByOthers: questionTotals.deltaPercentageByOther,
+            competenceTotAvgBySelf: competenceTotals.averageBySelfAssessment,
+            competenceTotAvgByTeam: competenceTotals.averageByTeam,
+            competenceTotAvgByOthers: competenceTotals.averageByOther,
+            competenceTotPctBySelf: competenceTotals.percentageBySelfAssessment,
+            competenceTotPctByTeam: competenceTotals.percentageByTeam,
+            competenceTotPctByOthers: competenceTotals.percentageByOther,
+            competenceTotDeltaPctByTeam: competenceTotals.deltaPercentageByTeam,
+            competenceTotDeltaPctByOthers:
+                competenceTotals.deltaPercentageByOther,
+            analytics: [],
+            comments: [],
+        });
+ 
+        const createdReport = await this.reports.create(report);
+ 
+        await this.analyticsRepo.createMany(createdReport.id!, [
+            ...questionAnalytics,
+            ...competenceAnalytics,
+        ]);
+ 
+        await this.saveClusterScoresForCompetences(
+            competenceAnalytics,
+            answers,
+            relations,
+            review,
+        );
+ 
+        this.logger.debug(
+            `Generated ${questionAnalytics.length} question analytics and ${competenceAnalytics.length} competence analytics`,
+        );
+ 
+        const questionInsights = await this.calculateEntityInsights(
+            questionAnalytics,
+            EntityType.QUESTION,
+            createdReport.id!,
+        );
+ 
+        const competenceInsights = await this.calculateEntityInsights(
+            competenceAnalytics,
+            EntityType.COMPETENCE,
+            createdReport.id!,
+        );
+ 
+        await this.insightsRepo.createMany(createdReport.id!, [
+            ...questionInsights,
+            ...competenceInsights,
+        ]);
+ 
+        this.logger.debug(
+            `Generated ${questionInsights.length} question insights and ${competenceInsights.length} competence insights`,
+        );
+ 
+        this.logger.debug(
+            `Successfully created report ${createdReport.id} for review ${reviewId}`,
+        );
+ 
+        await this.reviews.updateById(reviewId, {
+            reportId: createdReport.id,
+        });
+ 
+        return createdReport;
+    }
+ 
+    /**
+     * Verifies that the anonymity threshold is met for the given review.
+     * Anonymity threshold is the minimum number of answers
+     * required for a review to be valid for report generation.
+     * If the threshold is not met, an exception is thrown.
+     * @param review The review.
+     * @param answers The actual answers.
+     */
+    private async verifyAnonimityThreshold(
+        review: ReviewDomain,
+        answersCounts: {
+            respondentCategory: RespondentCategory;
+            answers: number;
+        }[],
+    ) {
+        let anonimityThreshold;
+ 
+        if (review.cycleId) {
+            const cycle = await this.cyclesRepo.findById(review.cycleId);
+            anonimityThreshold = cycle?.minRespondentsThreshold;
+        } else {
+            anonimityThreshold = CYCLE_CONSTRAINTS.ANONYMITY_THRESHOLD.MIN;
+        }
+ 
+        let isAnonimityThresholdMet: {
+            category: RespondentCategory;
+            met: boolean;
+        }[] = [];
+ 
+        if (
+            answersCounts.length === 1 &&
+            answersCounts[0].respondentCategory ===
+                RespondentCategory.SELF_ASSESSMENT
+        ) {
+            throw new NotFoundException(
+                `Not enough answers in category to meet the anonymity threshold ${anonimityThreshold} for review ${review.id}: only self assessment answers available.`,
+            );
+        }
+ 
+        answersCounts.forEach((stat) => {
+            if (
+                stat.respondentCategory === RespondentCategory.SELF_ASSESSMENT
+            ) {
+                return;
+            }
+ 
+            isAnonimityThresholdMet.push({
+                category: stat.respondentCategory,
+                met: stat.answers >= anonimityThreshold,
+            });
+        });
+ 
+        if (isAnonimityThresholdMet.some((item) => !item.met)) {
+            throw new NotFoundException(
+                `Not enough answers in category to meet the anonymity threshold ${anonimityThreshold} for review ${review.id}: ${JSON.stringify(isAnonimityThresholdMet)}`,
+            );
+        }
+    }
+ 
+    /**
+     * Saves the cluster scores for the competences.
+     * @param competenceAnalytics The competence analytics.
+     * @param answers The answers.
+     * @param relations The review-question relations.
+     * @param review The review.
+     */
+    private async saveClusterScoresForCompetences(
+        competenceAnalytics: ReportAnalyticsDomain[],
+        answers: AnswerDomain[],
+        relations: ReviewQuestionRelationDomain[],
+        review: ReviewDomain,
+    ): Promise<void> {
+        const questionToCompetence = new Map<number, number>();
+        for (const relation of relations) {
+            Eif (
+                relation.answerType === AnswerType.NUMERICAL_SCALE &&
+                relation.competenceId !== null &&
+                relation.competenceId !== undefined
+            ) {
+                questionToCompetence.set(
+                    relation.questionId,
+                    relation.competenceId,
+                );
+            }
+        }
+ 
+        const competenceAnswerCounts = new Map<number, number>();
+        for (const answer of answers) {
+            Iif (
+                answer.respondentCategory === RespondentCategory.SELF_ASSESSMENT
+            )
+                continue;
+            const competenceId = questionToCompetence.get(answer.questionId);
+            Iif (competenceId === undefined) continue;
+            Iif (
+                answer.numericalValue === null ||
+                answer.numericalValue === undefined
+            )
+                continue;
+            const current = competenceAnswerCounts.get(competenceId) ?? 0;
+            competenceAnswerCounts.set(competenceId, current + 1);
+        }
+ 
+        for (const analytic of competenceAnalytics) {
+            const competenceId = analytic.competenceId;
+            Iif (competenceId === null || competenceId === undefined) continue;
+ 
+            const finalScore = this.calculateCompetenceFinalScore(
+                analytic.averageByTeam ?? null,
+                analytic.averageByOther ?? null,
+            );
+            const roundedScore = this.roundDecimal(finalScore);
+            Iif (roundedScore === null) continue;
+ 
+            const clusters = await this.clusters.search({ competenceId });
+            Eif (!clusters.length) continue;
+ 
+            const matchedCluster = this.findMatchingCluster(
+                roundedScore,
+                clusters,
+            );
+            if (!matchedCluster) {
+                this.logger.debug(
+                    `Cluster not found for competence ${competenceId} and score ${roundedScore.toFixed(4)}`,
+                );
+                continue;
+            }
+ 
+            const answersCount = competenceAnswerCounts.get(competenceId) ?? 0;
+ 
+            await this.clusterScores.upsert(
+                ClusterScoreDomain.create({
+                    cycleId: review.cycleId ?? null,
+                    clusterId: matchedCluster.id!,
+                    rateeId: review.rateeId,
+                    reviewId: review.id!,
+                    score: roundedScore,
+                    answersCount,
+                }),
+            );
+        }
+    }
+ 
+    /**
+     * Calculates the final score for a competence.
+     * @param averageByTeam The average score by team.
+     * @param averageByOther The average score by other.
+     * @returns The final score.
+     */
+    private calculateCompetenceFinalScore(
+        averageByTeam: Decimal | null,
+        averageByOther: Decimal | null,
+    ): Decimal | null {
+        return this.calculateAverage([averageByTeam, averageByOther]);
+    }
+ 
+    /**
+     * Finds the matching cluster for a score.
+     * @param score The score to find the cluster for.
+     * @param clusters The clusters to search in.
+     * @returns The matching cluster.
+     */
+    private findMatchingCluster(
+        score: Decimal,
+        clusters: ClusterDomain[],
+    ): ClusterDomain | null {
+        for (const cluster of clusters) {
+            const lower = new Decimal(cluster.lowerBound);
+            const upper = new Decimal(cluster.upperBound);
+            const isMaxUpper = upper.equals(
+                new Decimal(REPORT_ANALYTICS_CONSTRAINTS.SCORE.MAX),
+            );
+            const meetsLower = score.greaterThanOrEqualTo(lower);
+            const meetsUpper = isMaxUpper
+                ? score.lessThanOrEqualTo(upper)
+                : score.lessThan(upper);
+            if (meetsLower && meetsUpper) {
+                return cluster;
+            }
+        }
+        return null;
+    }
+ 
+    /**
+     * Calculates average of decimals, returns null if array is empty.
+     * Formula: sum / count
+     * @param numbers The array of numbers to calculate the average of.
+     * @returns The average of the numbers as a Decimal, or null if the array is empty.
+     */
+    private calculateAverage(
+        numbers: (Decimal | number | null | undefined)[],
+    ): Decimal | null {
+        const validNumbers = numbers
+            .filter((n): n is Decimal | number => n !== null && n !== undefined)
+            .map((n) => (n instanceof Decimal ? n : new Decimal(n)));
+        if (validNumbers.length === 0) return null;
+        const sum = validNumbers.reduce(
+            (acc, value) => acc.plus(value),
+            new Decimal(0),
+        );
+        return sum.dividedBy(validNumbers.length);
+    }
+ 
+    /**
+     * Normalizes decimal scale to 4 digits (or null)
+     * @param value The value to normalize.
+     * @returns The normalized value.
+     */
+    private roundDecimal(
+        value: Decimal | number | null | undefined,
+    ): Decimal | null {
+        if (value === null || value === undefined) return null;
+        const decimalValue =
+            value instanceof Decimal ? value : new Decimal(value);
+        return decimalValue.toDecimalPlaces(4);
+    }
+ 
+    /**
+     * Calculates the turnout percentage for a specific category of respondents.
+     * Formula: (actual answers / assigned respondents) * 100
+     * @param respondents The list of all respondents.
+     * @param actualAnswers The count of actual answers for the category.
+     * @param category The category to calculate turnout for.
+     * @returns The turnout percentage as a Decimal, or null if no respondents in category.
+     */
+    private calculateTurnout(
+        respondents: RespondentDomain[],
+        actualAnswers: number,
+        category: RespondentCategory,
+    ): Decimal | null {
+        const assignedRespondents = respondents.filter(
+            (respondent) => respondent.category === category,
+        );
+ 
+        if (assignedRespondents.length === 0) {
+            return null;
+        }
+ 
+        Iif (actualAnswers === 0) {
+            return new Decimal(0);
+        }
+ 
+        const turnout = new Decimal(actualAnswers)
+            .dividedBy(assignedRespondents.length)
+            .times(100);
+ 
+        return this.roundDecimal(turnout);
+    }
+ 
+    /**
+     * Builds the analytics payload for a report, calculating averages and percentages for questions and competences.
+     * @param answers The answers to calculate analytics from.
+     * @param relations The review-question relations to determine allowed question IDs.
+     * @param maxScore The maximum score for the report.
+     * @returns An object containing question analytics, competence analytics, and summary totals.
+     */
+    private buildAnalyticsPayload(
+        answers: AnswerDomain[],
+        relations: ReviewQuestionRelationDomain[],
+        maxScore: Decimal,
+    ): {
+        questionAnalytics: ReportAnalyticsDomain[];
+        competenceAnalytics: ReportAnalyticsDomain[];
+        questionTotals: EntitySummaryTotals;
+        competenceTotals: EntitySummaryTotals;
+    } {
+        const numericalRelations = relations.filter(
+            (r) => r.answerType === AnswerType.NUMERICAL_SCALE,
+        );
+ 
+        const answersByQuestion = this.groupAnswersByQuestion(
+            answers,
+            numericalRelations,
+        );
+        const questionAnalytics: ReportAnalyticsDomain[] = [];
+        const competenceAccumulators = new Map<number, CompetenceAccumulator>();
+        const questionSelfAverages: Decimal[] = [];
+        const questionTeamAverages: Decimal[] = [];
+        const questionOtherAverages: Decimal[] = [];
+ 
+        for (const relation of numericalRelations) {
+            const questionAnswers =
+                answersByQuestion.get(relation.questionId) ?? [];
+ 
+            const averageBySelf = this.calculateAverageByCategory(
+                questionAnswers,
+                RespondentCategory.SELF_ASSESSMENT,
+            );
+            const averageByTeam = this.calculateAverageByCategory(
+                questionAnswers,
+                RespondentCategory.TEAM,
+            );
+            const averageByOther = this.calculateAverageByCategory(
+                questionAnswers,
+                RespondentCategory.OTHER,
+            );
+ 
+            Iif (averageBySelf !== null) {
+                questionSelfAverages.push(averageBySelf);
+            }
+            Eif (averageByTeam !== null) {
+                questionTeamAverages.push(averageByTeam);
+            }
+            Eif (averageByOther !== null) {
+                questionOtherAverages.push(averageByOther);
+            }
+ 
+            const percentageBySelf = this.calculatePercentage(
+                averageBySelf,
+                maxScore,
+            );
+            const percentageByTeam = this.calculatePercentage(
+                averageByTeam,
+                maxScore,
+            );
+            const percentageByOther = this.calculatePercentage(
+                averageByOther,
+                maxScore,
+            );
+ 
+            const deltaPercentageByTeam = this.calculateDeltaRating(
+                percentageByTeam,
+                percentageBySelf,
+            );
+            const deltaPercentageByOther = this.calculateDeltaRating(
+                percentageByOther,
+                percentageBySelf,
+            );
+ 
+            questionAnalytics.push(
+                ReportAnalyticsDomain.create({
+                    reportId: 0,
+                    entityType: EntityType.QUESTION,
+                    questionId: relation.questionId,
+                    questionTitle: relation.questionTitle,
+                    competenceId: null,
+                    competenceTitle: null,
+                    averageBySelfAssessment:
+                        this.roundDecimal(averageBySelf) ?? null,
+                    averageByTeam: this.roundDecimal(averageByTeam) ?? null,
+                    averageByOther: this.roundDecimal(averageByOther) ?? null,
+                    percentageBySelfAssessment:
+                        this.roundDecimal(percentageBySelf) ?? null,
+                    percentageByTeam:
+                        this.roundDecimal(percentageByTeam) ?? null,
+                    percentageByOther:
+                        this.roundDecimal(percentageByOther) ?? null,
+                    deltaPercentageByTeam:
+                        this.roundDecimal(deltaPercentageByTeam) ?? null,
+                    deltaPercentageByOther:
+                        this.roundDecimal(deltaPercentageByOther) ?? null,
+                }),
+            );
+ 
+            const competenceId = relation.competenceId;
+            Eif (competenceId !== null && competenceId !== undefined) {
+                const accumulator = competenceAccumulators.get(
+                    competenceId,
+                ) ?? {
+                    competenceId,
+                    competenceTitle: relation.competenceTitle ?? null,
+                    selfScores: [],
+                    teamScores: [],
+                    otherScores: [],
+                };
+ 
+                Iif (averageBySelf !== null) {
+                    accumulator.selfScores.push(averageBySelf);
+                }
+                Eif (averageByTeam !== null) {
+                    accumulator.teamScores.push(averageByTeam);
+                }
+                Eif (averageByOther !== null) {
+                    accumulator.otherScores.push(averageByOther);
+                }
+ 
+                competenceAccumulators.set(competenceId, accumulator);
+            }
+        }
+ 
+        const competenceAnalytics: ReportAnalyticsDomain[] = [];
+        const competenceSelfAverages: Decimal[] = [];
+        const competenceTeamAverages: Decimal[] = [];
+        const competenceOtherAverages: Decimal[] = [];
+ 
+        for (const accumulator of competenceAccumulators.values()) {
+            const averageBySelf = this.calculateAverage(
+                accumulator.selfScores.map((s) => new Decimal(s)),
+            );
+            const averageByTeam = this.calculateAverage(
+                accumulator.teamScores.map((s) => new Decimal(s)),
+            );
+            const averageByOther = this.calculateAverage(
+                accumulator.otherScores.map((s) => new Decimal(s)),
+            );
+ 
+            Iif (averageBySelf !== null) {
+                competenceSelfAverages.push(averageBySelf);
+            }
+            Eif (averageByTeam !== null) {
+                competenceTeamAverages.push(averageByTeam);
+            }
+            Eif (averageByOther !== null) {
+                competenceOtherAverages.push(averageByOther);
+            }
+ 
+            const percentageBySelf = this.calculatePercentage(
+                averageBySelf,
+                maxScore,
+            );
+            const percentageByTeam = this.calculatePercentage(
+                averageByTeam,
+                maxScore,
+            );
+            const percentageByOther = this.calculatePercentage(
+                averageByOther,
+                maxScore,
+            );
+ 
+            const deltaPercentageByTeam = this.calculateDeltaRating(
+                percentageByTeam,
+                percentageBySelf,
+            );
+            const deltaPercentageByOther = this.calculateDeltaRating(
+                percentageByOther,
+                percentageBySelf,
+            );
+ 
+            competenceAnalytics.push(
+                ReportAnalyticsDomain.create({
+                    reportId: 0,
+                    entityType: EntityType.COMPETENCE,
+                    questionId: null,
+                    questionTitle: null,
+                    competenceId: accumulator.competenceId,
+                    competenceTitle: accumulator.competenceTitle,
+                    averageBySelfAssessment:
+                        this.roundDecimal(averageBySelf) ?? null,
+                    averageByTeam: this.roundDecimal(averageByTeam) ?? null,
+                    averageByOther: this.roundDecimal(averageByOther) ?? null,
+                    percentageBySelfAssessment:
+                        this.roundDecimal(percentageBySelf) ?? null,
+                    percentageByTeam:
+                        this.roundDecimal(percentageByTeam) ?? null,
+                    percentageByOther:
+                        this.roundDecimal(percentageByOther) ?? null,
+                    deltaPercentageByTeam:
+                        this.roundDecimal(deltaPercentageByTeam) ?? null,
+                    deltaPercentageByOther:
+                        this.roundDecimal(deltaPercentageByOther) ?? null,
+                }),
+            );
+        }
+ 
+        return {
+            questionAnalytics,
+            competenceAnalytics,
+            questionTotals: this.calculateSummaryTotalsFromValues(
+                questionSelfAverages,
+                questionTeamAverages,
+                questionOtherAverages,
+                maxScore,
+            ),
+            competenceTotals: this.calculateSummaryTotalsFromValues(
+                competenceSelfAverages,
+                competenceTeamAverages,
+                competenceOtherAverages,
+                maxScore,
+            ),
+        };
+    }
+ 
+    /**
+     * Groups answers by question ID, filtering for numerical scale answers only.
+     * @param answers The answers to group.
+     * @param relations The review-question relations to determine allowed question IDs.
+     * @returns A map of question IDs to arrays of answers.
+     */
+    private groupAnswersByQuestion(
+        answers: AnswerDomain[],
+        relations: ReviewQuestionRelationDomain[],
+    ): Map<number, AnswerDomain[]> {
+        const allowedQuestionIds = new Set(
+            relations
+                .filter(
+                    (relation) =>
+                        relation.answerType === AnswerType.NUMERICAL_SCALE,
+                )
+                .map((relation) => relation.questionId),
+        );
+        const grouped = new Map<number, AnswerDomain[]>();
+        for (const answer of answers) {
+            Iif (!allowedQuestionIds.has(answer.questionId)) {
+                continue;
+            }
+            const bucket = grouped.get(answer.questionId) ?? [];
+            bucket.push(answer);
+            grouped.set(answer.questionId, bucket);
+        }
+        return grouped;
+    }
+ 
+    /**
+     * Calculates the average score for a specific category of respondents.
+     * @param answers The answers to calculate the average from.
+     * @param category The category of respondents.
+     * @returns The average score for the category.
+     */
+    private calculateAverageByCategory(
+        answers: AnswerDomain[],
+        category: RespondentCategory,
+    ): Decimal | null {
+        const values = answers
+            .filter(
+                (answer) =>
+                    answer.respondentCategory === category &&
+                    answer.numericalValue !== null &&
+                    answer.numericalValue !== undefined,
+            )
+            .map((answer) => new Decimal(answer.numericalValue!));
+        return this.calculateAverage(values);
+    }
+ 
+    /**
+     * Calculates the difference between two values in percentage points.
+     * Formula: respondent rating - self rating.
+     * @param respondent The respondent rating.
+     * @param self The self assessment rating.
+     * @returns The difference in percentage points.
+     */
+    private calculateDeltaRating(
+        respondent: Decimal | null,
+        self: Decimal | null,
+    ): Decimal | null {
+        Eif (respondent === null || self === null) {
+            return null;
+        }
+        return this.roundDecimal(respondent.minus(self));
+    }
+ 
+    /**
+     * Calculates the summary totals for a competence.
+     * @param analytics The analytics for the competence.
+     * @param maxScore The maximum score.
+     * @returns The summary totals.
+     */
+    private calculateCompetenceSummaryTotals(
+        analytics: ReportAnalyticsDomain[],
+        maxScore: Decimal,
+    ): EntitySummaryTotals {
+        const averageBySelf = this.calculateAverage(
+            analytics.map((item) => item.averageBySelfAssessment),
+        );
+        const averageByTeam = this.calculateAverage(
+            analytics.map((item) => item.averageByTeam),
+        );
+        const averageByOther = this.calculateAverage(
+            analytics.map((item) => item.averageByOther),
+        );
+ 
+        const percentageBySelf = this.calculatePercentage(
+            averageBySelf,
+            maxScore,
+        );
+        const percentageByTeam = this.calculatePercentage(
+            averageByTeam,
+            maxScore,
+        );
+        const percentageByOther = this.calculatePercentage(
+            averageByOther,
+            maxScore,
+        );
+ 
+        const deltaPercentageByTeam = this.calculateDeltaRating(
+            percentageByTeam,
+            percentageBySelf,
+        );
+        const deltaPercentageByOther = this.calculateDeltaRating(
+            percentageByOther,
+            percentageBySelf,
+        );
+        return {
+            averageBySelfAssessment: this.toRoundedNumber(averageBySelf),
+            averageByTeam: this.toRoundedNumber(averageByTeam),
+            averageByOther: this.toRoundedNumber(averageByOther),
+            percentageBySelfAssessment: this.toRoundedNumber(percentageBySelf),
+            percentageByTeam: this.toRoundedNumber(percentageByTeam),
+            percentageByOther: this.toRoundedNumber(percentageByOther),
+            deltaPercentageByTeam: this.toRoundedNumber(deltaPercentageByTeam),
+            deltaPercentageByOther: this.toRoundedNumber(
+                deltaPercentageByOther,
+            ),
+        };
+    }
+ 
+    /**
+     * Calculates the percentage of a value relative to a maximum score.
+     * Formula: (value / maxScore) * 100
+     * @param value The value to calculate the percentage of.
+     * @param maxScore The maximum score.
+     * @returns The percentage.
+     */
+    private calculatePercentage(
+        value: Decimal | null,
+        maxScore: Decimal,
+    ): Decimal | null {
+        if (value === null || maxScore.isZero()) {
+            return null;
+        }
+        return this.roundDecimal(value.dividedBy(maxScore).times(100));
+    }
+ 
+    /**
+     * Rounds a Decimal value to 4 decimal places.
+     * @param value The value to round.
+     * @returns The rounded value.
+     */
+    private toRoundedNumber(value: Decimal | null): number | null {
+        if (value === null) return null;
+        return Number(this.roundDecimal(value)?.toFixed(4));
+    }
+ 
+    /**
+     * Calculates the summary totals from arrays of values.
+     * @param selfValues The values for self-assessment.
+     * @param teamValues The values for team assessment.
+     * @param otherValues The values for other assessment.
+     * @param maxScore The maximum score.
+     * @returns The summary totals.
+     */
+    private calculateSummaryTotalsFromValues(
+        selfValues: Decimal[],
+        teamValues: Decimal[],
+        otherValues: Decimal[],
+        maxScore: Decimal,
+    ): EntitySummaryTotals {
+        const averageBySelf = this.calculateAverage(selfValues);
+        const averageByTeam = this.calculateAverage(teamValues);
+        const averageByOther = this.calculateAverage(otherValues);
+ 
+        const percentageBySelf = this.calculatePercentage(
+            averageBySelf,
+            maxScore,
+        );
+        const percentageByTeam = this.calculatePercentage(
+            averageByTeam,
+            maxScore,
+        );
+        const percentageByOther = this.calculatePercentage(
+            averageByOther,
+            maxScore,
+        );
+ 
+        const deltaPercentageByTeam = this.calculateDeltaRating(
+            percentageByTeam,
+            percentageBySelf,
+        );
+        const deltaPercentageByOther = this.calculateDeltaRating(
+            percentageByOther,
+            percentageBySelf,
+        );
+ 
+        return {
+            averageBySelfAssessment: this.toRoundedNumber(averageBySelf),
+            averageByTeam: this.toRoundedNumber(averageByTeam),
+            averageByOther: this.toRoundedNumber(averageByOther),
+            percentageBySelfAssessment: this.toRoundedNumber(percentageBySelf),
+            percentageByTeam: this.toRoundedNumber(percentageByTeam),
+            percentageByOther: this.toRoundedNumber(percentageByOther),
+            deltaPercentageByTeam: this.toRoundedNumber(deltaPercentageByTeam),
+            deltaPercentageByOther: this.toRoundedNumber(
+                deltaPercentageByOther,
+            ),
+        };
+    }
+ 
+    /**
+     * Calculates the metrics insights for entities (questions or competences).
+     * @param entities The entities to calculate the metrics insights for.
+     * @param type The type of entities (question or competence).
+     * @param reportId The ID of the report.
+     * @returns The metrics insights.
+     */
+    private calculateEntityInsights(
+        entities: ReportAnalyticsDomain[],
+        type: EntityType,
+        reportId: number,
+    ): ReportInsightDomain[] {
+        const averages: EntityAverageInsightsDto[] = [...entities]
+            .sort((a, b) => {
+                const nameA =
+                    type === EntityType.QUESTION
+                        ? (a.questionTitle ?? '')
+                        : (a.competenceTitle ?? '');
+                const nameB =
+                    type === EntityType.QUESTION
+                        ? (b.questionTitle ?? '')
+                        : (b.competenceTitle ?? '');
+                return nameA.localeCompare(nameB);
+            })
+            .map((entity, index) => {
+                return {
+                    entityId:
+                        type === EntityType.QUESTION
+                            ? entity.questionId
+                            : entity.competenceId,
+                    entityType: type,
+                    entityTitle:
+                        (type === EntityType.QUESTION
+                            ? entity.questionTitle
+                            : entity.competenceTitle) ?? '',
+                    num: index + 1,
+                    averageScore: this.getValidAverage([
+                        entity.averageByTeam,
+                        entity.averageByOther,
+                    ]),
+                    averageRating: this.getValidAverage([
+                        entity.percentageByTeam,
+                        entity.percentageByOther,
+                    ]),
+                    averageDelta: this.getValidAverage([
+                        entity.deltaPercentageByTeam,
+                        entity.deltaPercentageByOther,
+                    ]),
+                };
+            });
+ 
+        const validRatings = averages
+            .map((e) => e.averageRating)
+            .filter((v): v is number => v !== undefined && v !== null);
+        const validDeltas = averages
+            .map((e) => e.averageDelta)
+            .filter((v): v is number => v !== undefined && v !== null);
+ 
+        const maxRating =
+            validRatings.length > 0 ? Math.max(...validRatings) : null;
+        const minRating =
+            validRatings.length > 0 ? Math.min(...validRatings) : null;
+        const maxDelta =
+            validDeltas.length > 0 ? Math.max(...validDeltas) : null;
+        const minDelta =
+            validDeltas.length > 0 ? Math.min(...validDeltas) : null;
+ 
+        const insightSummaries: EntityInsightSummaryDto = {
+            highestRating:
+                maxRating !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageRating ?? 0).equals(maxRating),
+                      )
+                    : undefined,
+            lowestRating:
+                minRating !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageRating ?? 0).equals(minRating),
+                      )
+                    : undefined,
+            highestDelta:
+                maxDelta !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageDelta ?? 0).equals(maxDelta),
+                      )
+                    : undefined,
+            lowestDelta:
+                minDelta !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageDelta ?? 0).equals(minDelta),
+                      )
+                    : undefined,
+        };
+ 
+        const insights: ReportInsightDomain[] = [];
+ 
+        insights.push(
+            ReportInsightDomain.create({
+                reportId: reportId!,
+                insightType: InsightType.HIGHEST_RATING,
+                entityType: type,
+                questionId:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.highestRating?.entityId ?? null)
+                        : null,
+                questionTitle:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.highestRating?.entityTitle ?? null)
+                        : null,
+                competenceId:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.highestRating?.entityId ?? null)
+                        : null,
+                competenceTitle:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.highestRating?.entityTitle ?? null)
+                        : null,
+                averageScore: insightSummaries.highestRating?.averageScore,
+                averageRating: insightSummaries.highestRating?.averageRating,
+                averageDelta: insightSummaries.highestRating?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            ReportInsightDomain.create({
+                reportId: reportId!,
+                insightType: InsightType.LOWEST_RATING,
+                entityType: type,
+                questionId:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.lowestRating?.entityId ?? null)
+                        : null,
+                questionTitle:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.lowestRating?.entityTitle ?? null)
+                        : null,
+                competenceId:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.lowestRating?.entityId ?? null)
+                        : null,
+                competenceTitle:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.lowestRating?.entityTitle ?? null)
+                        : null,
+                averageScore: insightSummaries.lowestRating?.averageScore,
+                averageRating: insightSummaries.lowestRating?.averageRating,
+                averageDelta: insightSummaries.lowestRating?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            ReportInsightDomain.create({
+                reportId: reportId!,
+                insightType: InsightType.HIGHEST_DELTA,
+                entityType: type,
+                questionId:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.highestDelta?.entityId ?? null)
+                        : null,
+                questionTitle:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.highestDelta?.entityTitle ?? null)
+                        : null,
+                competenceId:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.highestDelta?.entityId ?? null)
+                        : null,
+                competenceTitle:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.highestDelta?.entityTitle ?? null)
+                        : null,
+                averageScore: insightSummaries.highestDelta?.averageScore,
+                averageRating: insightSummaries.highestDelta?.averageRating,
+                averageDelta: insightSummaries.highestDelta?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            ReportInsightDomain.create({
+                reportId: reportId!,
+                insightType: InsightType.LOWEST_DELTA,
+                entityType: type,
+                questionId:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.lowestDelta?.entityId ?? null)
+                        : null,
+                questionTitle:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.lowestDelta?.entityTitle ?? null)
+                        : null,
+                competenceId:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.lowestDelta?.entityId ?? null)
+                        : null,
+                competenceTitle:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.lowestDelta?.entityTitle ?? null)
+                        : null,
+                averageScore: insightSummaries.lowestDelta?.averageScore,
+                averageRating: insightSummaries.lowestDelta?.averageRating,
+                averageDelta: insightSummaries.lowestDelta?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        return insights;
+    }
+ 
+    /**
+     * Calculates the average of an array of numbers.
+     * @param arr The array of numbers.
+     * @returns The average of the numbers.
+     */
+    private getValidAverage = (arr: (Decimal | null | undefined)[]) => {
+        const validValues = arr.filter(
+            (val): val is Decimal => val !== null && val !== undefined,
+        );
+        if (validValues.length === 0) return null;
+        return this.calculateAverageNumberForArray(validValues);
+    };
+ 
+    /**
+     * Calculates the average of an array of numbers.
+     * @param numbers The array of numbers.
+     * @returns The average of the numbers.
+     */
+    private calculateAverageNumberForArray(numbers: Decimal[]): Decimal {
+        let sum = Decimal(0);
+        for (const number of numbers) {
+            sum = sum.add(number);
+        }
+        return sum.div(numbers.length);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/startegic-report-insight.service.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/startegic-report-insight.service.ts.html new file mode 100644 index 00000000..1a1bed9b --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/startegic-report-insight.service.ts.html @@ -0,0 +1,241 @@ + + + + + + Code coverage report for reporting/application/services/startegic-report-insight.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services startegic-report-insight.service.ts

+
+ +
+ 94.44% + Statements + 17/18 +
+ + +
+ 66.66% + Branches + 8/12 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 93.75% + Lines + 15/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +5317x +  +  +17x +  +  +  +17x +  +  +17x +  +  +17x +17x +  +  +  +  +  +  +4x +  +  +2x +  +2x +  +  +  +  +  +2x +  +  +  +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { StrategicReportInsightDomain } from '../../domain/startegic-report-insight.domain';
+import {
+    STRATEGIC_REPORT_INSIGHT_REPOSITORY,
+    StrategicReportInsightRepositoryPort,
+} from '../ports/strategic-report-insight.repository.port';
+import { StrategicReportingService } from './strategic-reports.service';
+ 
+@Injectable()
+export class StartegicReportInsightService {
+    constructor(
+        @Inject(STRATEGIC_REPORT_INSIGHT_REPOSITORY)
+        private readonly insights: StrategicReportInsightRepositoryPort,
+        private readonly reportingService: StrategicReportingService,
+    ) {}
+ 
+    async getByStrategicReportId(
+        strategicReportId: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportInsightDomain[]> {
+        await this.reportingService.getById(strategicReportId, actor);
+ 
+        const insights =
+            await this.insights.findByStrategicReportId(strategicReportId);
+ 
+        Iif (!insights) {
+            throw new NotFoundException(
+                `Report insights with strategic report id ${strategicReportId} was not found`,
+            );
+        }
+ 
+        return insights;
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportInsightDomain> {
+        const insight = await this.insights.findById(id);
+ 
+        if (!insight) {
+            throw new NotFoundException(
+                `Report insight with id ${id} was not found`,
+            );
+        }
+ 
+        await this.reportingService.getById(insight.strategicReportId, actor);
+ 
+        return insight;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/strategic-report-analytics.service.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/strategic-report-analytics.service.ts.html new file mode 100644 index 00000000..39352d9e --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/strategic-report-analytics.service.ts.html @@ -0,0 +1,256 @@ + + + + + + Code coverage report for reporting/application/services/strategic-report-analytics.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services strategic-report-analytics.service.ts

+
+ +
+ 94.44% + Statements + 17/18 +
+ + +
+ 66.66% + Branches + 8/12 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 93.75% + Lines + 15/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +5817x +  +  +17x +  +  +  +17x +  +  +17x +  +  +17x +17x +  +  +  +  +  +  +4x +  +  +2x +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { StrategicReportAnalyticsDomain } from '../../domain/strategic-report-analytics.domain';
+import {
+    STRATEGIC_REPORT_ANALYTICS_REPOSITORY,
+    StrategicReportAnalyticsRepositoryPort,
+} from '../ports/strategic-report-analytics.repository.port';
+import { StrategicReportingService } from './strategic-reports.service';
+ 
+@Injectable()
+export class StrategicReportAnalyticsService {
+    constructor(
+        @Inject(STRATEGIC_REPORT_ANALYTICS_REPOSITORY)
+        private readonly strategicAnalytics: StrategicReportAnalyticsRepositoryPort,
+        private readonly strategicReportingService: StrategicReportingService,
+    ) {}
+ 
+    async getByStrategicReportId(
+        strategicReportId: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportAnalyticsDomain[]> {
+        await this.strategicReportingService.getById(strategicReportId, actor);
+ 
+        const strategicAnalytics =
+            await this.strategicAnalytics.findByStrategicReportId(
+                strategicReportId,
+            );
+ 
+        Iif (!strategicAnalytics) {
+            throw new NotFoundException(
+                `Strategic report analytics with report id ${strategicReportId} was not found`,
+            );
+        }
+ 
+        return strategicAnalytics;
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportAnalyticsDomain> {
+        const strategicAnalytics = await this.strategicAnalytics.findById(id);
+ 
+        if (!strategicAnalytics) {
+            throw new NotFoundException(
+                `Strategic report analytics with id ${id} was not found`,
+            );
+        }
+ 
+        await this.strategicReportingService.getById(
+            strategicAnalytics.strategicReportId,
+            actor,
+        );
+ 
+        return strategicAnalytics;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/strategic-reports.service.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/strategic-reports.service.ts.html new file mode 100644 index 00000000..280a4f30 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/strategic-reports.service.ts.html @@ -0,0 +1,2953 @@ + + + + + + Code coverage report for reporting/application/services/strategic-reports.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services strategic-reports.service.ts

+
+ +
+ 85.71% + Statements + 252/294 +
+ + +
+ 53.87% + Branches + 125/232 +
+ + +
+ 79.03% + Functions + 49/62 +
+ + +
+ 88.29% + Lines + 249/282 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +95717x +  +  +  +  +  +  +  +  +  +  +17x +  +  +  +  +  +  +17x +17x +  +  +  +17x +  +  +  +17x +  +  +  +  +  +17x +  +  +  +  +17x +  +  +  +  +17x +  +  +  +17x +  +  +  +  +  +17x +  +  +  +17x +  +  +  +17x +  +  +  +  +  +  +  +17x +17x +17x +17x +  +  +  +17x +  +  +  +17x +  +  +  +17x +  +  +  +17x +  +  +  +  +  +17x +17x +  +  +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +17x +  +  +  +  +  +  +2x +1x +  +  +  +  +  +  +  +2x +  +  +2x +  +  +2x +1x +  +  +  +  +  +  +  +  +  +12x +  +12x +3x +  +  +  +  +9x +  +7x +  +  +  +  +  +  +  +  +  +  +  +10x +  +  +10x +  +  +10x +  +2x +  +2x +  +  +  +  +2x +2x +  +  +2x +  +2x +  +  +  +  +  +  +  +  +2x +2x +1x +  +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +  +5x +  +5x +5x +1x +  +  +4x +4x +1x +1x +  +  +3x +3x +1x +  +  +  +  +2x +2x +  +  +  +  +  +  +2x +  +2x +2x +2x +2x +2x +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +2x +2x +2x +2x +  +  +  +  +2x +2x +2x +2x +2x +  +2x +2x +2x +2x +2x +  +2x +2x +2x +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +2x +  +  +  +2x +2x +  +2x +2x +  +  +  +  +2x +  +  +  +2x +2x +  +  +  +2x +  +  +  +  +  +  +2x +  +2x +  +  +  +2x +2x +2x +  +2x +  +  +  +  +  +2x +2x +2x +2x +2x +  +  +2x +2x +2x +  +  +  +2x +2x +  +2x +2x +2x +  +  +  +  +2x +2x +  +  +2x +  +2x +  +  +  +  +  +  +  +  +  +2x +2x +  +  +  +  +2x +  +  +  +  +2x +2x +  +2x +  +  +2x +2x +6x +  +2x +  +  +2x +2x +  +  +2x +  +  +2x +  +  +  +2x +2x +  +2x +2x +  +2x +2x +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +2x +  +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +12x +12x +12x +12x +12x +12x +  +  +12x +  +  +  +  +  +  +  +  +  +  +52x +  +52x +52x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +2x +2x +2x +2x +2x +2x +  +  +  +  +2x +2x +  +2x +2x +  +2x +2x +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +  +2x +2x +  +2x +2x +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +  +  +12x +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +  +  +  +8x +  +  +  +  +  +  +  +  +16x +16x +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +2x +2x +2x +2x +  +  +2x +  +2x +  +2x +  +2x +  +2x +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +2x +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +17x +6x +12x +  +6x +6x +  +  +  +  +  +  +  +  +6x +6x +12x +  +6x +  +  + 
import {
+    EntityAverageInsightsDto,
+    EntityInsightSummaryDto,
+    EntitySummaryTotals,
+    EntityType,
+    IdentityRole,
+    InsightType,
+    REPORT_ANALYTICS_CONSTRAINTS,
+    RespondentCategory,
+    StrategicReportSearchQuery,
+} from '@intra/shared-kernel';
+import {
+    ForbiddenException,
+    Inject,
+    Injectable,
+    Logger,
+    NotFoundException,
+} from '@nestjs/common';
+import Decimal from 'decimal.js';
+import {
+    CYCLE_REPOSITORY,
+    CycleRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/cycle.repository.port';
+import {
+    QUESTION_REPOSITORY,
+    QuestionRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/question.repository.port';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/reviewer.repository.port';
+import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain';
+import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain';
+import {
+    IDENTITY_USER_REPOSITORY,
+    UserRepositoryPort,
+} from 'src/contexts/identity/application/ports/user.repository.port';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import {
+    COMPETENCE_REPOSITORY,
+    CompetenceRepositoryPort,
+} from 'src/contexts/library/application/ports/competence.repository.port';
+import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain';
+import {
+    ORGANISATION_POSITION_REPOSITORY,
+    PositionRepositoryPort,
+} from 'src/contexts/organisation/application/ports/position.repository.port';
+import {
+    ORGANISATION_TEAM_REPOSITORY,
+    TeamRepositoryPort,
+} from 'src/contexts/organisation/application/ports/team.repository.port';
+import { PositionDomain } from 'src/contexts/organisation/domain/position.domain';
+import { TeamDomain } from 'src/contexts/organisation/domain/team.domain';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../../../feedback360/application/ports/answer.repository.port';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../../../feedback360/application/ports/respondent.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../../../feedback360/application/ports/review.repository.port';
+import { AnswerDomain } from '../../../feedback360/domain/answer.domain';
+import { RespondentDomain } from '../../../feedback360/domain/respondent.domain';
+import { ReviewDomain } from '../../../feedback360/domain/review.domain';
+import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+import { StrategicReportInsightDomain } from '../../domain/startegic-report-insight.domain';
+import { StrategicReportAnalyticsDomain } from '../../domain/strategic-report-analytics.domain';
+import { StrategicReportDomain } from '../../domain/strategic-report.domain';
+import {
+    REPORT_ANALYTICS_REPOSITORY,
+    ReportAnalyticsRepositoryPort,
+} from '../ports/report-analytics.repository.port';
+import {
+    REPORT_REPOSITORY,
+    ReportRepositoryPort,
+} from '../ports/report.repository.port';
+import {
+    STRATEGIC_REPORT_ANALYTICS_REPOSITORY,
+    StrategicReportAnalyticsRepositoryPort,
+} from '../ports/strategic-report-analytics.repository.port';
+import {
+    STRATEGIC_REPORT_INSIGHT_REPOSITORY,
+    StrategicReportInsightRepositoryPort,
+} from '../ports/strategic-report-insight.repository.port';
+import {
+    STRATEGIC_REPORT_REPOSITORY,
+    StrategicReportRepositoryPort,
+} from '../ports/strategic-report.repository.port';
+ 
+@Injectable()
+export class StrategicReportingService {
+    private readonly logger = new Logger(StrategicReportingService.name);
+ 
+    constructor(
+        @Inject(STRATEGIC_REPORT_REPOSITORY)
+        private readonly strategicReports: StrategicReportRepositoryPort,
+        @Inject(STRATEGIC_REPORT_ANALYTICS_REPOSITORY)
+        private readonly strategicAnalyticsRepo: StrategicReportAnalyticsRepositoryPort,
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        @Inject(REVIEWER_REPOSITORY)
+        private readonly reviewers: ReviewerRepositoryPort,
+        @Inject(ANSWER_REPOSITORY)
+        private readonly answers: AnswerRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        @Inject(CYCLE_REPOSITORY)
+        private readonly cyclesRepo: CycleRepositoryPort,
+        @Inject(REPORT_REPOSITORY)
+        private readonly reports: ReportRepositoryPort,
+        @Inject(IDENTITY_USER_REPOSITORY)
+        private readonly users: UserRepositoryPort,
+        @Inject(ORGANISATION_TEAM_REPOSITORY)
+        private readonly teams: TeamRepositoryPort,
+        @Inject(ORGANISATION_POSITION_REPOSITORY)
+        private readonly positions: PositionRepositoryPort,
+        @Inject(COMPETENCE_REPOSITORY)
+        private readonly competences: CompetenceRepositoryPort,
+        @Inject(QUESTION_REPOSITORY)
+        private readonly questions: QuestionRepositoryPort,
+        @Inject(REPORT_ANALYTICS_REPOSITORY)
+        private readonly reportAnalyticsRepo: ReportAnalyticsRepositoryPort,
+        @Inject(STRATEGIC_REPORT_INSIGHT_REPOSITORY)
+        private readonly insightsRepo: StrategicReportInsightRepositoryPort,
+    ) {}
+ 
+    async search(
+        query: StrategicReportSearchQuery,
+        actor?: UserDomain,
+    ): Promise<StrategicReportDomain[]> {
+        await this.checkAccessToAllStrategicReports(actor);
+        return await this.strategicReports.search(query);
+    }
+ 
+    /**
+     * Checks if the actor has access to all strategic reports (HR or Admin only).
+     * @param actor The actor to check access for.
+     */
+    async checkAccessToAllStrategicReports(actor?: UserDomain): Promise<void> {
+        Iif (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (!isAdminOrHr) {
+            throw new ForbiddenException(
+                'You do not have permission to view strategic reports',
+            );
+        }
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportDomain> {
+        const report = await this.strategicReports.findById(id);
+ 
+        if (!report) {
+            throw new NotFoundException(
+                `Strategic report with id ${id} was not found`,
+            );
+        }
+ 
+        await this.checkAccessToStrategicReport(report, actor);
+ 
+        return report;
+    }
+ 
+    /**
+     * Checks if the actor has access to the strategic report.
+     * @param report The strategic report to check access for.
+     * @param actor The actor to check access for.
+     */
+    async checkAccessToStrategicReport(
+        report: StrategicReportDomain,
+        actor?: UserDomain,
+    ): Promise<void> {
+        Iif (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const reviews = await this.reviews.search({ cycleId: report.cycleId });
+ 
+        Iif (!reviews || reviews.length === 0) {
+            throw new NotFoundException(`Reviews for strategic report ${report.id} was not found.
+                Unable to check access to strategic report`);
+        }
+ 
+        const isManagerOfReview = reviews.some(
+            (review) => review.managerId === actor.id,
+        );
+ 
+        Iif (isManagerOfReview) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view this strategic report',
+        );
+    }
+ 
+    async getByCycleId(
+        cycleId: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportDomain> {
+        const report = await this.strategicReports.findByCycleId(cycleId);
+        if (!report) {
+            throw new NotFoundException(
+                `Report for cycle ${cycleId} was not found`,
+            );
+        }
+ 
+        await this.checkAccessToStrategicReport(report, actor);
+ 
+        return report;
+    }
+ 
+    /**
+     * Generates a comprehensive strategic report for a feedback360 cycle.
+     * @param cycleId The ID of the cycle to generate a report for.
+     * @returns The generated report.
+     */
+    async generateStrategicReportForCycle(
+        cycleId: number,
+    ): Promise<StrategicReportDomain> {
+        this.logger.debug(`Starting report generation for cycle ${cycleId}`);
+ 
+        const cycle = await this.cyclesRepo.findById(cycleId);
+        if (!cycle) {
+            throw new NotFoundException(`Cycle ${cycleId} not found`);
+        }
+ 
+        const existing = await this.strategicReports.findByCycleId(cycleId);
+        if (existing) {
+            this.logger.debug(`Report already exists for cycle ${cycleId}`);
+            return existing;
+        }
+ 
+        const reports = await this.reports.search({ cycleId });
+        if (!reports || reports.length === 0) {
+            throw new NotFoundException(
+                `Reports for cycle ${cycleId} not found`,
+            );
+        }
+ 
+        const allReviews = new Set<ReviewDomain>();
+        const reviewIds = new Set<number>();
+ 
+        const turnouts: {
+            reportId: number;
+            rateeTurnout: Decimal;
+            teamTurnout: Decimal;
+            otherTurnout: Decimal;
+        }[] = [];
+ 
+        for (const report of reports) {
+            const review = await this.reviews.findById(report.reviewId);
+            Eif (review) {
+                allReviews.add(review);
+                reviewIds.add(review.id!);
+            }
+ 
+            turnouts.push({
+                reportId: report.id!,
+                rateeTurnout: new Decimal(0),
+                teamTurnout: report.turnoutPctOfTeam
+                    ? new Decimal(report.turnoutPctOfTeam)
+                    : new Decimal(0),
+                otherTurnout: report.turnoutPctOfOther
+                    ? new Decimal(report.turnoutPctOfOther)
+                    : new Decimal(0),
+            });
+        }
+ 
+        const allRatees = new Set<UserDomain>();
+        const allRespondents = new Set<RespondentDomain>();
+        const allAnswers = new Set<AnswerDomain>();
+        const actualAnswersByCategory: Record<string, Decimal> = {
+            [RespondentCategory.SELF_ASSESSMENT]: new Decimal(0),
+            [RespondentCategory.TEAM]: new Decimal(0),
+            [RespondentCategory.OTHER]: new Decimal(0),
+        };
+        const allReviewers = new Set<ReviewerDomain>();
+        const allTeams = new Map<number, TeamDomain>();
+        const allPositions = new Map<number, PositionDomain>();
+        const allCompetences = new Map<number, CompetenceDomain>();
+        const allQuestions = new Map<number, QuestionDomain>();
+ 
+        for (const review of allReviews) {
+            const teamIds = new Set<number>();
+            const positionIds = new Set<number>();
+            const competenceIds = new Set<number>();
+            const questionIds = new Set<number>();
+ 
+            const ratee = await this.users.findById(review.rateeId);
+            Eif (ratee) {
+                allRatees.add(ratee);
+ 
+                Iif (ratee.teamId) {
+                    teamIds.add(ratee.teamId);
+                }
+ 
+                Iif (ratee.positionId) {
+                    positionIds.add(ratee.positionId);
+                }
+            }
+ 
+            const respondents = await this.respondents.listByReview(
+                review.id!,
+                {},
+            );
+            respondents.forEach((respondent) => {
+                allRespondents.add(respondent);
+ 
+                if (respondent.teamId) {
+                    teamIds.add(respondent.teamId);
+                }
+            });
+ 
+            for (const teamId of teamIds) {
+                if (!allTeams.has(teamId)) {
+                    const team = await this.teams.findById(teamId);
+                    if (team) {
+                        allTeams.set(teamId, team);
+                    }
+                }
+            }
+ 
+            for (const positionId of positionIds) {
+                if (!allPositions.has(positionId)) {
+                    const position = await this.positions.findById(positionId);
+                    if (position) {
+                        allPositions.set(positionId, position);
+                    }
+                }
+            }
+ 
+            const answers = await this.answers.list({
+                reviewId: review.id!,
+            });
+ 
+            answers.forEach((answer) => {
+                allAnswers.add(answer);
+ 
+                Eif (answer.questionId) {
+                    questionIds.add(answer.questionId);
+                }
+            });
+ 
+            const answersCounts =
+                await this.answers.getAnswersCountByRespondentCategories(
+                    review.id!,
+                );
+ 
+            answersCounts.forEach((stat) => {
+                Eif (
+                    actualAnswersByCategory[stat.respondentCategory] !==
+                    undefined
+                ) {
+                    actualAnswersByCategory[stat.respondentCategory] =
+                        actualAnswersByCategory[stat.respondentCategory].plus(
+                            stat.answers,
+                        );
+                }
+            });
+ 
+            const selfAssessmentStat = answersCounts.find(
+                (stat) =>
+                    stat.respondentCategory ===
+                    RespondentCategory.SELF_ASSESSMENT,
+            );
+            const hasSelfAssessment =
+                selfAssessmentStat && selfAssessmentStat.answers > 0;
+            const currentTurnout = turnouts.find(
+                (t) => t.reportId === review.reportId,
+            );
+            Iif (currentTurnout) {
+                currentTurnout.rateeTurnout = hasSelfAssessment
+                    ? new Decimal(100)
+                    : new Decimal(0);
+            }
+ 
+            for (const questionId of questionIds) {
+                Eif (!allQuestions.has(questionId)) {
+                    const question = await this.questions.findById(questionId);
+                    Eif (question) {
+                        allQuestions.set(questionId, question);
+                    }
+                }
+                const q = allQuestions.get(questionId);
+                Eif (q && q.competenceId) {
+                    competenceIds.add(q.competenceId);
+                }
+            }
+ 
+            for (const competenceId of competenceIds) {
+                Eif (!allCompetences.has(competenceId)) {
+                    const competence =
+                        await this.competences.findById(competenceId);
+                    Eif (competence) {
+                        allCompetences.set(competenceId, competence);
+                    }
+                }
+            }
+ 
+            const reviewers = await this.reviewers.listByReview(review.id!, {});
+            reviewers.forEach((reviewer) => allReviewers.add(reviewer));
+        }
+ 
+        const allReportAnalytics = await Promise.all(
+            reports.map(async (report) => {
+                return {
+                    reportId: report.id!,
+                    reportAnalytics:
+                        await this.reportAnalyticsRepo.findByReportId(
+                            report.id!,
+                        ),
+                };
+            }),
+        );
+ 
+        const maxScore = new Decimal(REPORT_ANALYTICS_CONSTRAINTS.SCORE.MAX);
+        const competenceAnalytics = this.buildStrategicAnalyticPayload(
+            allReportAnalytics,
+            maxScore,
+            Array.from(allCompetences.values()),
+        );
+        const competenceTotals = this.calculateCompetenceSummaryTotals(
+            competenceAnalytics,
+            maxScore,
+        );
+ 
+        const allRateeIds = [
+            ...new Set<number>([...allRatees].map((r) => r.id!)),
+        ].sort((a, b) => a - b);
+        const allRespondentIds = [
+            ...new Set<number>([...allRespondents].map((r) => r.respondentId!)),
+        ].sort((a, b) => a - b);
+        let allAnswerCount = new Decimal(0);
+        Object.values(actualAnswersByCategory).forEach((count) => {
+            allAnswerCount = allAnswerCount.plus(count);
+        });
+        const allReviewerIds = [
+            ...new Set<number>([...allReviewers].map((r) => r.reviewerId!)),
+        ].sort((a, b) => a - b);
+        const allTeamIds = Array.from(allTeams.keys()).sort((a, b) => a - b);
+        const allPositionIds = Array.from(allPositions.keys()).sort(
+            (a, b) => a - b,
+        );
+        const allCompetenceIds = Array.from(allCompetences.keys()).sort(
+            (a, b) => a - b,
+        );
+        const allQuestionIds = Array.from(allQuestions.keys()).sort(
+            (a, b) => a - b,
+        );
+ 
+        const rateeTurnout = turnouts
+            .reduce((acc, t) => acc.plus(t.rateeTurnout), new Decimal(0))
+            .dividedBy(turnouts.length);
+        const teamTurnout = turnouts
+            .reduce((acc, t) => acc.plus(t.teamTurnout), new Decimal(0))
+            .dividedBy(turnouts.length);
+        const otherTurnout = turnouts
+            .reduce((acc, t) => acc.plus(t.otherTurnout), new Decimal(0))
+            .dividedBy(turnouts.length);
+ 
+        const report = StrategicReportDomain.create({
+            cycleId: cycleId,
+            cycleTitle: cycle.title,
+            rateeCount: allRateeIds.length,
+            rateeIds: allRateeIds,
+            respondentCount: allRespondentIds.length,
+            respondentIds: allRespondentIds,
+            answerCount: allAnswerCount.toNumber(),
+            reviewerCount: allReviewerIds.length,
+            reviewerIds: allReviewerIds,
+            teamCount: allTeamIds.length,
+            teamIds: allTeamIds,
+            positionCount: allPositionIds.length,
+            positionIds: allPositionIds,
+            competenceCount: allCompetenceIds.length,
+            competenceIds: allCompetenceIds,
+            questionCount: allQuestionIds.length,
+            questionIds: allQuestionIds,
+            turnoutAvgPctOfRatees: rateeTurnout,
+            turnoutAvgPctOfTeams: teamTurnout,
+            turnoutAvgPctOfOthers: otherTurnout,
+            competenceGeneralAvgSelf: competenceTotals.averageBySelfAssessment,
+            competenceGeneralAvgTeam: competenceTotals.averageByTeam,
+            competenceGeneralAvgOther: competenceTotals.averageByOther,
+            competenceGeneralPctSelf:
+                competenceTotals.percentageBySelfAssessment,
+            competenceGeneralPctTeam: competenceTotals.percentageByTeam,
+            competenceGeneralPctOther: competenceTotals.percentageByOther,
+            competenceGeneralDeltaTeam: competenceTotals.deltaPercentageByTeam,
+            competenceGeneralDeltaOther:
+                competenceTotals.deltaPercentageByOther,
+            analytics: [],
+        });
+ 
+        const createdReport = await this.strategicReports.create(report);
+ 
+        await this.strategicAnalyticsRepo.createMany(createdReport.id!, [
+            ...competenceAnalytics,
+        ]);
+ 
+        this.logger.debug(
+            `Generated ${competenceAnalytics.length} strategic report analytics for report ${createdReport.id}`,
+        );
+ 
+        const competenceInsights = await this.calculateEntityInsights(
+            competenceAnalytics,
+            createdReport.id!,
+        );
+ 
+        await this.insightsRepo.createMany(createdReport.id!, [
+            ...competenceInsights,
+        ]);
+ 
+        this.logger.debug(
+            `Generated ${competenceInsights.length} competence insights`,
+        );
+ 
+        const fullReport = await this.strategicReports.findById(
+            createdReport.id!,
+        );
+        Iif (!fullReport) {
+            throw new NotFoundException(
+                `Strategic report ${createdReport.id} could not be loaded after creation`,
+            );
+        }
+ 
+        this.logger.debug(
+            `Successfully created strategic report ${fullReport.id} for cycle ${cycleId}`,
+        );
+ 
+        return fullReport;
+    }
+ 
+    /**
+     * Calculates average of decimals, returns null if array is empty.
+     * Formula: sum / count
+     * @param numbers The array of numbers to calculate the average of.
+     * @returns The average of the numbers as a Decimal, or null if the array is empty.
+     */
+    private calculateAverage(
+        numbers: (Decimal | number | null | undefined)[],
+    ): Decimal | null {
+        const validNumbers = numbers
+            .filter((n): n is Decimal | number => n !== null && n !== undefined)
+            .map((n) => (n instanceof Decimal ? n : new Decimal(n)));
+        Iif (validNumbers.length === 0) return null;
+        const sum = validNumbers.reduce(
+            (acc, value) => acc.plus(value),
+            new Decimal(0),
+        );
+        return sum.dividedBy(validNumbers.length);
+    }
+ 
+    /**
+     * Normalizes decimal scale to 4 digits (or null)
+     * @param value The value to normalize.
+     * @returns The normalized value.
+     */
+    private roundDecimal(
+        value: Decimal | number | null | undefined,
+    ): Decimal | null {
+        Iif (value === null || value === undefined) return null;
+        const decimalValue =
+            value instanceof Decimal ? value : new Decimal(value);
+        return decimalValue.toDecimalPlaces(4);
+    }
+ 
+    /**
+     * Calculates the turnout percentage of respondents.
+     * Formula: (actual answers / assigned respondents) * 100
+     * @param respondentCount The count of all assigned respondents.
+     * @param actualAnswers The count of actual answers for the category.
+     * @returns The turnout percentage as a Decimal.
+     */
+    private calculateTurnout(
+        respondentCount: Decimal,
+        actualAnswers: Decimal,
+    ): Decimal {
+        if (actualAnswers?.equals(0)) {
+            return new Decimal(0);
+        }
+ 
+        const turnout = actualAnswers.dividedBy(respondentCount).times(100);
+ 
+        return this.roundDecimal(turnout) ?? new Decimal(0);
+    }
+ 
+    /**
+     * Builds the strategic analytics payload for a report, calculating averages
+     * and percentages for competences from all reports.
+     * @param reportAnalytics The report analytics for each report.
+     * @param maxScore The maximum score for the competences.
+     * @param allCompetences The list of all rated competences.
+     * @returns An object containing competence analytics.
+     */
+    private buildStrategicAnalyticPayload(
+        reportAnalytics: {
+            reportId: number;
+            reportAnalytics: ReportAnalyticsDomain[];
+        }[],
+        maxScore: Decimal,
+        allCompetences: CompetenceDomain[],
+    ): StrategicReportAnalyticsDomain[] {
+        const competenceAnalytics: StrategicReportAnalyticsDomain[] = [];
+ 
+        for (const competence of allCompetences) {
+            const currentCompetenceScores: ReportAnalyticsDomain[] = [];
+            reportAnalytics.forEach((r) => {
+                r.reportAnalytics?.forEach((a) => {
+                    Eif (a.competenceId === competence.id) {
+                        currentCompetenceScores.push(a);
+                    }
+                });
+            });
+ 
+            const averageBySelf = this.calculateAverage(
+                currentCompetenceScores.map((a) => a.averageBySelfAssessment),
+            );
+            const averageByTeam = this.calculateAverage(
+                currentCompetenceScores.map((a) => a.averageByTeam),
+            );
+            const averageByOther = this.calculateAverage(
+                currentCompetenceScores.map((a) => a.averageByOther),
+            );
+ 
+            const percentageBySelf = this.calculatePercentage(
+                averageBySelf,
+                maxScore,
+            );
+            const percentageByTeam = this.calculatePercentage(
+                averageByTeam,
+                maxScore,
+            );
+            const percentageByOther = this.calculatePercentage(
+                averageByOther,
+                maxScore,
+            );
+ 
+            const deltaPercentageByTeam = this.calculateDelta(
+                percentageByTeam,
+                percentageBySelf,
+            );
+            const deltaPercentageByOther = this.calculateDelta(
+                percentageByOther,
+                percentageBySelf,
+            );
+ 
+            competenceAnalytics.push(
+                StrategicReportAnalyticsDomain.create({
+                    strategicReportId: 0,
+                    competenceId: competence.id!,
+                    competenceTitle: competence.title!,
+                    averageBySelfAssessment:
+                        this.roundDecimal(averageBySelf) ?? null,
+                    averageByTeam: this.roundDecimal(averageByTeam) ?? null,
+                    averageByOther: this.roundDecimal(averageByOther) ?? null,
+                    percentageBySelfAssessment:
+                        this.roundDecimal(percentageBySelf) ?? null,
+                    percentageByTeam:
+                        this.roundDecimal(percentageByTeam) ?? null,
+                    percentageByOther:
+                        this.roundDecimal(percentageByOther) ?? null,
+                    deltaPercentageByTeam:
+                        this.roundDecimal(deltaPercentageByTeam) ?? null,
+                    deltaPercentageByOther:
+                        this.roundDecimal(deltaPercentageByOther) ?? null,
+                }),
+            );
+        }
+ 
+        return competenceAnalytics;
+    }
+ 
+    /**
+     * Calculates the summary totals for all rated competences.
+     * @param strategicAnalytics The strategic analytics for the competences.
+     * @param maxScore The maximum score.
+     * @returns The summary competence totals.
+     */
+    private calculateCompetenceSummaryTotals(
+        strategicAnalytics: StrategicReportAnalyticsDomain[],
+        maxScore: Decimal,
+    ): EntitySummaryTotals {
+        const averageBySelf = this.calculateAverage(
+            strategicAnalytics.map((item) => item.averageBySelfAssessment),
+        );
+        const averageByTeam = this.calculateAverage(
+            strategicAnalytics.map((item) => item.averageByTeam),
+        );
+        const averageByOther = this.calculateAverage(
+            strategicAnalytics.map((item) => item.averageByOther),
+        );
+ 
+        const percentageBySelf = this.calculatePercentage(
+            averageBySelf,
+            maxScore,
+        );
+        const percentageByTeam = this.calculatePercentage(
+            averageByTeam,
+            maxScore,
+        );
+        const percentageByOther = this.calculatePercentage(
+            averageByOther,
+            maxScore,
+        );
+ 
+        const deltaPercentageByTeam = this.calculateDelta(
+            percentageByTeam,
+            percentageBySelf,
+        );
+        const deltaPercentageByOther = this.calculateDelta(
+            percentageByOther,
+            percentageBySelf,
+        );
+        return {
+            averageBySelfAssessment: this.toRoundedNumber(averageBySelf),
+            averageByTeam: this.toRoundedNumber(averageByTeam),
+            averageByOther: this.toRoundedNumber(averageByOther),
+            percentageBySelfAssessment: this.toRoundedNumber(percentageBySelf),
+            percentageByTeam: this.toRoundedNumber(percentageByTeam),
+            percentageByOther: this.toRoundedNumber(percentageByOther),
+            deltaPercentageByTeam: this.toRoundedNumber(deltaPercentageByTeam),
+            deltaPercentageByOther: this.toRoundedNumber(
+                deltaPercentageByOther,
+            ),
+        };
+    }
+ 
+    /**
+     * Calculates the percentage of a value relative to a maximum score.
+     * Formula: (value / maxScore) * 100
+     * @param value The value to calculate the percentage of.
+     * @param maxScore The maximum score.
+     * @returns The percentage.
+     */
+    private calculatePercentage(
+        value: Decimal | null,
+        maxScore: Decimal,
+    ): Decimal | null {
+        Iif (value === null || maxScore.isZero()) {
+            return null;
+        }
+        return this.roundDecimal(value.dividedBy(maxScore).times(100));
+    }
+ 
+    /**
+     * Calculates the difference between two values in percentage points.
+     * Formula: respondent rating - self rating.
+     * @param respondent The respondent rating.
+     * @param self The self rating.
+     * @returns The difference in percentage points.
+     */
+    private calculateDelta(
+        respondent: Decimal | null,
+        self: Decimal | null,
+    ): Decimal | null {
+        Iif (respondent === null || self === null) {
+            return null;
+        }
+ 
+        return this.roundDecimal(respondent.minus(self));
+    }
+ 
+    /**
+     * Rounds a Decimal value to 4 decimal places.
+     * @param value The value to round.
+     * @returns The rounded value.
+     */
+    private toRoundedNumber(value: Decimal | null): number | null {
+        Iif (value === null) return null;
+        return Number(this.roundDecimal(value)?.toFixed(4));
+    }
+ 
+    /**
+     * Calculates the metrics insights for entities (questions or competences).
+     * @param entities The entities to calculate the metrics insights for.
+     * @param type The type of entities (question or competence).
+     * @param reportId The ID of the report.
+     * @returns The metrics insights.
+     */
+    private calculateEntityInsights(
+        entities: StrategicReportAnalyticsDomain[],
+        strategicReportId: number,
+    ): StrategicReportInsightDomain[] {
+        const averages: EntityAverageInsightsDto[] = [...entities]
+            .sort((a, b) => {
+                const nameA = a.competenceTitle ?? '';
+                const nameB = b.competenceTitle ?? '';
+                return nameA.localeCompare(nameB);
+            })
+            .map((entity, index) => {
+                return {
+                    entityId: entity.competenceId,
+                    entityType: EntityType.COMPETENCE,
+                    entityTitle: entity.competenceTitle,
+                    questionId: null,
+                    questionTitle: null,
+                    num: index + 1,
+                    averageScore: this.getValidAverage([
+                        entity.averageByTeam,
+                        entity.averageByOther,
+                    ]),
+                    averageRating: this.getValidAverage([
+                        entity.percentageByTeam,
+                        entity.percentageByOther,
+                    ]),
+                    averageDelta: this.getValidAverage([
+                        entity.deltaPercentageByTeam,
+                        entity.deltaPercentageByOther,
+                    ]),
+                };
+            });
+ 
+        const validRatings = averages
+            .map((e) => e.averageRating)
+            .filter((v): v is number => v !== undefined && v !== null);
+        const validDeltas = averages
+            .map((e) => e.averageDelta)
+            .filter((v): v is number => v !== undefined && v !== null);
+ 
+        const maxRating =
+            validRatings.length > 0 ? Math.max(...validRatings) : null;
+        const minRating =
+            validRatings.length > 0 ? Math.min(...validRatings) : null;
+        const maxDelta =
+            validDeltas.length > 0 ? Math.max(...validDeltas) : null;
+        const minDelta =
+            validDeltas.length > 0 ? Math.min(...validDeltas) : null;
+ 
+        const insightSummaries: EntityInsightSummaryDto = {
+            highestRating:
+                maxRating !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageRating ?? 0).equals(maxRating),
+                      )
+                    : undefined,
+            lowestRating:
+                minRating !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageRating ?? 0).equals(minRating),
+                      )
+                    : undefined,
+            highestDelta:
+                maxDelta !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageDelta ?? 0).equals(maxDelta),
+                      )
+                    : undefined,
+            lowestDelta:
+                minDelta !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageDelta ?? 0).equals(minDelta),
+                      )
+                    : undefined,
+        };
+ 
+        const insights: StrategicReportInsightDomain[] = [];
+ 
+        insights.push(
+            StrategicReportInsightDomain.create({
+                strategicReportId: strategicReportId!,
+                insightType: InsightType.HIGHEST_RATING,
+                competenceId: insightSummaries.highestRating?.entityId ?? -1,
+                competenceTitle:
+                    insightSummaries.highestRating?.entityTitle ?? '',
+                averageScore: insightSummaries.highestRating?.averageScore,
+                averageRating: insightSummaries.highestRating?.averageRating,
+                averageDelta: insightSummaries.highestRating?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            StrategicReportInsightDomain.create({
+                strategicReportId: strategicReportId!,
+                insightType: InsightType.LOWEST_RATING,
+                competenceId: insightSummaries.lowestRating?.entityId ?? -1,
+                competenceTitle:
+                    insightSummaries.lowestRating?.entityTitle ?? '',
+                averageScore: insightSummaries.lowestRating?.averageScore,
+                averageRating: insightSummaries.lowestRating?.averageRating,
+                averageDelta: insightSummaries.lowestRating?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            StrategicReportInsightDomain.create({
+                strategicReportId: strategicReportId!,
+                insightType: InsightType.HIGHEST_DELTA,
+                competenceId: insightSummaries.highestDelta?.entityId ?? -1,
+                competenceTitle:
+                    insightSummaries.highestDelta?.entityTitle ?? '',
+                averageScore: insightSummaries.highestDelta?.averageScore,
+                averageRating: insightSummaries.highestDelta?.averageRating,
+                averageDelta: insightSummaries.highestDelta?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            StrategicReportInsightDomain.create({
+                strategicReportId: strategicReportId!,
+                insightType: InsightType.LOWEST_DELTA,
+                competenceId: insightSummaries.lowestDelta?.entityId ?? -1,
+                competenceTitle:
+                    insightSummaries.lowestDelta?.entityTitle ?? '',
+                averageScore: insightSummaries.lowestDelta?.averageScore,
+                averageRating: insightSummaries.lowestDelta?.averageRating,
+                averageDelta: insightSummaries.lowestDelta?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        return insights;
+    }
+ 
+    /**
+     * Calculates the average of an array of numbers.
+     * @param arr The array of numbers.
+     * @returns The average of the numbers.
+     */
+    private getValidAverage = (arr: (Decimal | null | undefined)[]) => {
+        const validValues = arr.filter(
+            (val): val is Decimal => val !== null && val !== undefined,
+        );
+        Iif (validValues.length === 0) return null;
+        return this.calculateAverageNumberForArray(validValues);
+    };
+ 
+    /**
+     * Calculates the average of an array of numbers.
+     * @param numbers The array of numbers.
+     * @returns The average of the numbers.
+     */
+    private calculateAverageNumberForArray(numbers: Decimal[]): Decimal {
+        let sum = Decimal(0);
+        for (const number of numbers) {
+            sum = sum.add(number);
+        }
+        return sum.div(numbers.length);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/application/services/text-answer.service.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/text-answer.service.ts.html new file mode 100644 index 00000000..ff37eaf8 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/application/services/text-answer.service.ts.html @@ -0,0 +1,322 @@ + + + + + + Code coverage report for reporting/application/services/text-answer.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services text-answer.service.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 77.77% + Branches + 14/18 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 20/20 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +8017x +  +  +  +  +17x +  +17x +  +  +  +17x +  +  +  +  +  +  +17x +  +  +17x +  +17x +  +  +  +  +  +  +6x +  +5x +  +  +  +  +  +  +  +5x +5x +5x +  +  +5x +  +7x +  +  +5x +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x +  +  +5x +  +  +5x +  +1x +  +  +  +  + 
import {
+    AnswerType,
+    IdentityRole,
+    ReportTextAnswerDto,
+} from '@intra/shared-kernel';
+import { ForbiddenException, Inject, Injectable } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../../../feedback360/application/ports/answer.repository.port';
+import {
+    REVIEW_QUESTION_RELATION_REPOSITORY,
+    ReviewQuestionRelationRepositoryPort,
+} from '../../../feedback360/application/ports/review-question-relation.repository.port';
+import { ReviewQuestionRelationDomain } from '../../../feedback360/domain/review-question-relation.domain';
+ 
+@Injectable()
+export class TextAnswerService {
+    constructor(
+        @Inject(ANSWER_REPOSITORY)
+        private readonly answers: AnswerRepositoryPort,
+        @Inject(REVIEW_QUESTION_RELATION_REPOSITORY)
+        private readonly relations: ReviewQuestionRelationRepositoryPort,
+    ) {}
+ 
+    async listByReview(
+        reviewId: number,
+        actor?: UserDomain,
+    ): Promise<ReportTextAnswerDto[]> {
+        await this.checkAccessToTextAnswers(actor);
+ 
+        const [answers, relations] = await Promise.all([
+            this.answers.list({
+                reviewId,
+                answerType: AnswerType.TEXT_FIELD,
+            }),
+            this.relations.listByReview(reviewId, {}),
+        ]);
+ 
+        const relationMap = new Map<number, ReviewQuestionRelationDomain>();
+        relations.forEach((relation) => {
+            relationMap.set(relation.questionId, relation);
+        });
+ 
+        return answers
+            .filter(
+                (answer) => answer.textValue && answer.textValue.trim() !== '',
+            )
+            .map((answer) => {
+                const relation = relationMap.get(answer.questionId);
+                return {
+                    questionId: answer.questionId,
+                    questionTitle: relation?.questionTitle ?? null,
+                    respondentCategory: answer.respondentCategory,
+                    textValue: answer.textValue!,
+                };
+            });
+    }
+ 
+    /**
+     * Checks if the actor has access to the text answers
+     * as an admin or hr.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToTextAnswers(actor?: UserDomain): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view text answers for this review',
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/index.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/index.html new file mode 100644 index 00000000..783c77f5 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/index.html @@ -0,0 +1,206 @@ + + + + + + Code coverage report for reporting/infrastructure/mappers + + + + + + + + + +
+
+

All files reporting/infrastructure/mappers

+
+ +
+ 88.75% + Statements + 71/80 +
+ + +
+ 88.88% + Branches + 32/36 +
+ + +
+ 84.09% + Functions + 37/44 +
+ + +
+ 88.75% + Lines + 71/80 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
report-analytics.mapper.ts +
+
100%10/1091.66%11/12100%5/5100%10/10
report-comment.mapper.ts +
+
83.33%10/1250%2/4100%6/683.33%10/12
report-insight.mapper.ts +
+
100%12/1291.66%11/12100%7/7100%12/12
report.mapper.ts +
+
82.35%14/17100%0/072.72%8/1182.35%14/17
strategic-report-analytics.mapper.ts +
+
100%8/8100%4/4100%3/3100%8/8
strategic-report-insight.mapper.ts +
+
83.33%10/12100%4/471.42%5/783.33%10/12
strategic-report.mapper.ts +
+
77.77%7/9100%0/060%3/577.77%7/9
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-analytics.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-analytics.mapper.ts.html new file mode 100644 index 00000000..1929d8ef --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-analytics.mapper.ts.html @@ -0,0 +1,355 @@ + + + + + + Code coverage report for reporting/infrastructure/mappers/report-analytics.mapper.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/mappers report-analytics.mapper.ts

+
+ +
+ 100% + Statements + 10/10 +
+ + +
+ 91.66% + Branches + 11/12 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 100% + Lines + 10/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91  +  +  +  +  +  +7x +7x +  +7x +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +3x +  +  +  +  +  +562x +525x +  +37x +  +  + 
import {
+    Prisma,
+    EntityType as PrismaEntityType,
+    ReportAnalytics as PrismaReportAnalytics,
+} from '@intra/database';
+import { EntityType } from '@intra/shared-kernel';
+import Decimal from 'decimal.js';
+import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+ 
+export class ReportAnalyticsMapper {
+    static toDomain(analytics: PrismaReportAnalytics): ReportAnalyticsDomain {
+        return ReportAnalyticsDomain.create({
+            id: analytics.id,
+            reportId: analytics.reportId,
+            entityType: ReportAnalyticsMapper.toDomainEntityType(
+                analytics.entityType,
+            ),
+            questionId: analytics.questionId,
+            questionTitle: analytics.questionTitle,
+            competenceId: analytics.competenceId,
+            competenceTitle: analytics.competenceTitle,
+            averageBySelfAssessment: analytics.averageBySelfAssessment,
+            averageByTeam: analytics.averageByTeam,
+            averageByOther: analytics.averageByOther,
+            percentageBySelfAssessment: analytics.percentageBySelfAssessment,
+            percentageByTeam: analytics.percentageByTeam,
+            percentageByOther: analytics.percentageByOther,
+            deltaPercentageByTeam: analytics.deltaPercentageByTeam,
+            deltaPercentageByOther: analytics.deltaPercentageByOther,
+            createdAt: analytics.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        reportId: number,
+        analytics: ReportAnalyticsDomain,
+    ): Prisma.ReportAnalyticsCreateManyInput {
+        return {
+            reportId,
+            entityType: ReportAnalyticsMapper.toPrismaEntityType(
+                analytics.entityType,
+            ),
+            questionId: analytics.questionId ?? undefined,
+            questionTitle: analytics.questionTitle ?? undefined,
+            competenceId: analytics.competenceId ?? undefined,
+            competenceTitle: analytics.competenceTitle ?? undefined,
+            averageBySelfAssessment: ReportAnalyticsMapper.toDecimalString(
+                analytics.averageBySelfAssessment,
+            ),
+            averageByTeam: ReportAnalyticsMapper.toDecimalString(
+                analytics.averageByTeam,
+            ),
+            averageByOther: ReportAnalyticsMapper.toDecimalString(
+                analytics.averageByOther,
+            ),
+            percentageBySelfAssessment: ReportAnalyticsMapper.toDecimalString(
+                analytics.percentageBySelfAssessment,
+            ),
+            percentageByTeam: ReportAnalyticsMapper.toDecimalString(
+                analytics.percentageByTeam,
+            ),
+            percentageByOther: ReportAnalyticsMapper.toDecimalString(
+                analytics.percentageByOther,
+            ),
+            deltaPercentageByTeam: ReportAnalyticsMapper.toDecimalString(
+                analytics.deltaPercentageByTeam,
+            ),
+            deltaPercentageByOther: ReportAnalyticsMapper.toDecimalString(
+                analytics.deltaPercentageByOther,
+            ),
+        };
+    }
+ 
+    static toPrismaEntityType(domainType: EntityType): PrismaEntityType {
+        return domainType.toString().toUpperCase() as PrismaEntityType;
+    }
+ 
+    static toDomainEntityType(prismaType: PrismaEntityType): EntityType {
+        return prismaType.toString().toUpperCase() as EntityType;
+    }
+ 
+    static toDecimalString(
+        value: Decimal.Value | null | undefined,
+    ): string | undefined {
+        if (value === null || value === undefined) {
+            return undefined;
+        }
+        return new Decimal(value).toDecimalPlaces(4).toFixed(4);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-comment.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-comment.mapper.ts.html new file mode 100644 index 00000000..88763d6b --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-comment.mapper.ts.html @@ -0,0 +1,304 @@ + + + + + + Code coverage report for reporting/infrastructure/mappers/report-comment.mapper.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/mappers report-comment.mapper.ts

+
+ +
+ 83.33% + Statements + 10/12 +
+ + +
+ 50% + Branches + 2/4 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 83.33% + Lines + 10/12 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74  +  +  +  +  +  +  +7x +  +7x +  +8x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +16x +  +  +  +  +  +  +5x +  +  +5x +  +  +  +  +  +  +  +8x +  +  +8x +  +  + 
import {
+    Prisma,
+    CommentSentiment as PrismaCommentSentiment,
+    ReportComment as PrismaReportComment,
+    RespondentCategory as PrismaRespondentCategory,
+} from '@intra/database';
+import { CommentSentiment, RespondentCategory } from '@intra/shared-kernel';
+import { ReportCommentDomain } from '../../domain/report-comment.domain';
+ 
+export class ReportCommentMapper {
+    static toDomain(comment: PrismaReportComment): ReportCommentDomain {
+        return ReportCommentDomain.create({
+            id: comment.id,
+            reportId: comment.reportId,
+            questionId: comment.questionId,
+            questionTitle: comment.questionTitle,
+            comment: comment.comment,
+            respondentCategories:
+                ReportCommentMapper.toDomainRespondentCategories(
+                    comment.respondentCategories,
+                ),
+            commentSentiment: ReportCommentMapper.toDomainCommentSentiment(
+                comment.commentSentiment,
+            ),
+            numberOfMentions: comment.numberOfMentions,
+            createdAt: comment.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        comment: ReportCommentDomain,
+    ): Prisma.ReportCommentUncheckedCreateInput {
+        return {
+            reportId: comment.reportId,
+            questionId: comment.questionId,
+            questionTitle: comment.questionTitle,
+            comment: comment.comment,
+            respondentCategories: comment.respondentCategories,
+            commentSentiment: ReportCommentMapper.toPrismaCommentSentiment(
+                comment.commentSentiment,
+            ),
+            numberOfMentions: comment.numberOfMentions,
+        };
+    }
+ 
+    static toDomainRespondentCategories(
+        respondentCategories: PrismaRespondentCategory[],
+    ): RespondentCategory[] {
+        return respondentCategories.map(
+            (c) => c.toString().toUpperCase() as RespondentCategory,
+        );
+    }
+ 
+    static toPrismaCommentSentiment(
+        domainSentiment: CommentSentiment | null | undefined,
+    ): PrismaCommentSentiment | undefined {
+        Iif (!domainSentiment) {
+            return undefined;
+        }
+        return domainSentiment
+            .toString()
+            .toUpperCase() as PrismaCommentSentiment;
+    }
+ 
+    static toDomainCommentSentiment(
+        prismaSentiment: PrismaCommentSentiment | null,
+    ): CommentSentiment | undefined {
+        Iif (!prismaSentiment) {
+            return undefined;
+        }
+        return prismaSentiment.toString().toUpperCase() as CommentSentiment;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-insight.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-insight.mapper.ts.html new file mode 100644 index 00000000..1ae08093 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report-insight.mapper.ts.html @@ -0,0 +1,340 @@ + + + + + + Code coverage report for reporting/infrastructure/mappers/report-insight.mapper.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/mappers report-insight.mapper.ts

+
+ +
+ 100% + Statements + 12/12 +
+ + +
+ 91.66% + Branches + 11/12 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 12/12 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86  +  +  +  +  +  +  +7x +7x +  +7x +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +3x +  +  +  +5x +  +  +  +3x +  +  +  +  +  +15x +10x +  +5x +  +  + 
import {
+    Prisma,
+    EntityType as PrismaEntityType,
+    InsightType as PrismaInsightType,
+    ReportInsights as PrismaReportInsights,
+} from '@intra/database';
+import { EntityType, InsightType } from '@intra/shared-kernel';
+import Decimal from 'decimal.js';
+import { ReportInsightDomain } from '../../domain/report-insight.domain';
+ 
+export class ReportInsightMapper {
+    static toDomain(analytics: PrismaReportInsights): ReportInsightDomain {
+        return ReportInsightDomain.create({
+            id: analytics.id,
+            reportId: analytics.reportId,
+            insightType: ReportInsightMapper.toDomainInsightType(
+                analytics.insightType,
+            ),
+            entityType: ReportInsightMapper.toDomainEntityType(
+                analytics.entityType,
+            ),
+            questionId: analytics.questionId,
+            questionTitle: analytics.questionTitle,
+            competenceId: analytics.competenceId,
+            competenceTitle: analytics.competenceTitle,
+            averageScore: analytics.averageScore,
+            averageRating: analytics.averageRating,
+            averageDelta: analytics.averageDelta,
+            createdAt: analytics.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        reportId: number,
+        insights: ReportInsightDomain,
+    ): Prisma.ReportInsightsCreateManyInput {
+        return {
+            reportId,
+            insightType: ReportInsightMapper.toPrismaInsightType(
+                insights.insightType,
+            ),
+            entityType: ReportInsightMapper.toPrismaEntityType(
+                insights.entityType,
+            ),
+            questionId: insights.questionId ?? undefined,
+            questionTitle: insights.questionTitle ?? undefined,
+            competenceId: insights.competenceId ?? undefined,
+            competenceTitle: insights.competenceTitle ?? undefined,
+            averageScore: ReportInsightMapper.toDecimalString(
+                insights.averageScore,
+            ),
+            averageRating: ReportInsightMapper.toDecimalString(
+                insights.averageRating,
+            ),
+            averageDelta: ReportInsightMapper.toDecimalString(
+                insights.averageDelta,
+            ),
+        };
+    }
+ 
+    static toPrismaEntityType(domainType: EntityType): PrismaEntityType {
+        return domainType.toString().toUpperCase() as PrismaEntityType;
+    }
+ 
+    static toDomainEntityType(prismaType: PrismaEntityType): EntityType {
+        return prismaType.toString().toUpperCase() as EntityType;
+    }
+ 
+    static toPrismaInsightType(domainType: InsightType): PrismaInsightType {
+        return domainType.toString().toUpperCase() as PrismaInsightType;
+    }
+ 
+    static toDomainInsightType(prismaType: PrismaInsightType): InsightType {
+        return prismaType.toString().toUpperCase() as InsightType;
+    }
+ 
+    static toDecimalString(
+        value: Decimal.Value | null | undefined,
+    ): string | undefined {
+        if (value === null || value === undefined) {
+            return undefined;
+        }
+        return new Decimal(value).toDecimalPlaces(4).toFixed(4);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report.mapper.ts.html new file mode 100644 index 00000000..a310a737 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/report.mapper.ts.html @@ -0,0 +1,613 @@ + + + + + + Code coverage report for reporting/infrastructure/mappers/report.mapper.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/mappers report.mapper.ts

+
+ +
+ 82.35% + Statements + 14/17 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 72.72% + Functions + 8/11 +
+ + +
+ 82.35% + Lines + 14/17 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177  +  +  +  +  +  +  +  +7x +7x +7x +7x +7x +  +7x +  +6x +  +  +  +  +  +18x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +31x +  +  +  +  +  +63x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +29x +  +  +  +  +57x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +81x +  +  +  +  +  +57x +  +  + 
import {
+    Prisma,
+    Report as PrismaReport,
+    ReportAnalytics as PrismaReportAnalytics,
+    ReportComment as PrismaReportComment,
+    ReportInsights as PrismaReportInsight,
+    RespondentCategory as PrismaRespondentCategory,
+} from '@intra/database';
+import { RespondentCategory } from '@intra/shared-kernel';
+import { ReportDomain } from '../../domain/report.domain';
+import { ReportAnalyticsMapper } from './report-analytics.mapper';
+import { ReportCommentMapper } from './report-comment.mapper';
+import { ReportInsightMapper } from './report-insight.mapper';
+ 
+export class ReportMapper {
+    static toDomain(report: PrismaReport): ReportDomain {
+        return ReportDomain.create({
+            id: report.id,
+            reviewId: report.reviewId,
+            cycleId: report.cycleId,
+            respondentCount: report.respondentCount,
+            respondentCategories: report.respondentCategories.map((c) =>
+                ReportMapper.toDomainRespondentCategory(c),
+            ),
+            answerCount: report.answerCount,
+            turnoutPctOfTeam: report.turnoutPctOfTeam,
+            turnoutPctOfOther: report.turnoutPctOfOther,
+            questionTotAvgBySelf: report.questionTotAvgBySelf,
+            questionTotAvgByTeam: report.questionTotAvgByTeam,
+            questionTotAvgByOthers: report.questionTotAvgByOthers,
+            questionTotPctBySelf: report.questionTotPctBySelf,
+            questionTotPctByTeam: report.questionTotPctByTeam,
+            questionTotPctByOthers: report.questionTotPctByOthers,
+            questionTotDeltaPctByTeam: report.questionTotDeltaPctByTeam,
+            questionTotDeltaPctByOthers: report.questionTotDeltaPctByOthers,
+            competenceTotAvgBySelf: report.competenceTotAvgBySelf,
+            competenceTotAvgByTeam: report.competenceTotAvgByTeam,
+            competenceTotAvgByOthers: report.competenceTotAvgByOthers,
+            competenceTotPctBySelf: report.competenceTotPctBySelf,
+            competenceTotPctByTeam: report.competenceTotPctByTeam,
+            competenceTotPctByOthers: report.competenceTotPctByOthers,
+            competenceTotDeltaPctByTeam: report.competenceTotDeltaPctByTeam,
+            competenceTotDeltaPctByOthers: report.competenceTotDeltaPctByOthers,
+            createdAt: report.createdAt,
+            analytics: [],
+            comments: [],
+            insights: [],
+        });
+    }
+ 
+    static toDomainWithRelations(
+        report: PrismaReport & {
+            analytics: PrismaReportAnalytics[];
+            comments: PrismaReportComment[];
+            insights: PrismaReportInsight[];
+        },
+    ): ReportDomain {
+        return ReportDomain.create({
+            id: report.id,
+            reviewId: report.reviewId,
+            cycleId: report.cycleId,
+            respondentCount: report.respondentCount,
+            respondentCategories: report.respondentCategories.map((c) =>
+                ReportMapper.toDomainRespondentCategory(c),
+            ),
+            answerCount: report.answerCount,
+            turnoutPctOfTeam: report.turnoutPctOfTeam,
+            turnoutPctOfOther: report.turnoutPctOfOther,
+            questionTotAvgBySelf: report.questionTotAvgBySelf,
+            questionTotAvgByTeam: report.questionTotAvgByTeam,
+            questionTotAvgByOthers: report.questionTotAvgByOthers,
+            questionTotPctBySelf: report.questionTotPctBySelf,
+            questionTotPctByTeam: report.questionTotPctByTeam,
+            questionTotPctByOthers: report.questionTotPctByOthers,
+            questionTotDeltaPctByTeam: report.questionTotDeltaPctByTeam,
+            questionTotDeltaPctByOthers: report.questionTotDeltaPctByOthers,
+            competenceTotAvgBySelf: report.competenceTotAvgBySelf,
+            competenceTotAvgByTeam: report.competenceTotAvgByTeam,
+            competenceTotAvgByOthers: report.competenceTotAvgByOthers,
+            competenceTotPctBySelf: report.competenceTotPctBySelf,
+            competenceTotPctByTeam: report.competenceTotPctByTeam,
+            competenceTotPctByOthers: report.competenceTotPctByOthers,
+            competenceTotDeltaPctByTeam: report.competenceTotDeltaPctByTeam,
+            competenceTotDeltaPctByOthers: report.competenceTotDeltaPctByOthers,
+            createdAt: report.createdAt,
+            analytics: report.analytics.map((a) =>
+                ReportAnalyticsMapper.toDomain(a),
+            ),
+            comments: report.comments.map((c) =>
+                ReportCommentMapper.toDomain(c),
+            ),
+            insights: report.insights.map((i) =>
+                ReportInsightMapper.toDomain(i),
+            ),
+        });
+    }
+ 
+    static toPrisma(domain: ReportDomain): Prisma.ReportUncheckedCreateInput {
+        return {
+            reviewId: domain.reviewId,
+            cycleId: domain.cycleId,
+            respondentCount: domain.respondentCount,
+            respondentCategories: domain.respondentCategories.map((c) =>
+                ReportMapper.toPrismaRespondentCategory(c),
+            ),
+            answerCount: domain.answerCount,
+            turnoutPctOfTeam: ReportAnalyticsMapper.toDecimalString(
+                domain.turnoutPctOfTeam,
+            ),
+            turnoutPctOfOther: ReportAnalyticsMapper.toDecimalString(
+                domain.turnoutPctOfOther,
+            ),
+            questionTotAvgBySelf: ReportAnalyticsMapper.toDecimalString(
+                domain.questionTotAvgBySelf,
+            ),
+            questionTotAvgByTeam: ReportAnalyticsMapper.toDecimalString(
+                domain.questionTotAvgByTeam,
+            ),
+            questionTotAvgByOthers: ReportAnalyticsMapper.toDecimalString(
+                domain.questionTotAvgByOthers,
+            ),
+            questionTotPctBySelf: ReportAnalyticsMapper.toDecimalString(
+                domain.questionTotPctBySelf,
+            ),
+            questionTotPctByTeam: ReportAnalyticsMapper.toDecimalString(
+                domain.questionTotPctByTeam,
+            ),
+            questionTotPctByOthers: ReportAnalyticsMapper.toDecimalString(
+                domain.questionTotPctByOthers,
+            ),
+            questionTotDeltaPctByTeam: ReportAnalyticsMapper.toDecimalString(
+                domain.questionTotDeltaPctByTeam,
+            ),
+            questionTotDeltaPctByOthers: ReportAnalyticsMapper.toDecimalString(
+                domain.questionTotDeltaPctByOthers,
+            ),
+            competenceTotAvgBySelf: ReportAnalyticsMapper.toDecimalString(
+                domain.competenceTotAvgBySelf,
+            ),
+            competenceTotAvgByTeam: ReportAnalyticsMapper.toDecimalString(
+                domain.competenceTotAvgByTeam,
+            ),
+            competenceTotAvgByOthers: ReportAnalyticsMapper.toDecimalString(
+                domain.competenceTotAvgByOthers,
+            ),
+            competenceTotPctBySelf: ReportAnalyticsMapper.toDecimalString(
+                domain.competenceTotPctBySelf,
+            ),
+            competenceTotPctByTeam: ReportAnalyticsMapper.toDecimalString(
+                domain.competenceTotPctByTeam,
+            ),
+            competenceTotPctByOthers: ReportAnalyticsMapper.toDecimalString(
+                domain.competenceTotPctByOthers,
+            ),
+            competenceTotDeltaPctByTeam: ReportAnalyticsMapper.toDecimalString(
+                domain.competenceTotDeltaPctByTeam,
+            ),
+            competenceTotDeltaPctByOthers:
+                ReportAnalyticsMapper.toDecimalString(
+                    domain.competenceTotDeltaPctByOthers,
+                ),
+        };
+    }
+ 
+    static toDomainRespondentCategory(
+        category: PrismaRespondentCategory,
+    ): RespondentCategory {
+        return RespondentCategory[category];
+    }
+ 
+    static toPrismaRespondentCategory(
+        category: RespondentCategory,
+    ): PrismaRespondentCategory {
+        return category.toString().toUpperCase() as PrismaRespondentCategory;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report-analytics.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report-analytics.mapper.ts.html new file mode 100644 index 00000000..8a7e7f39 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report-analytics.mapper.ts.html @@ -0,0 +1,313 @@ + + + + + + Code coverage report for reporting/infrastructure/mappers/strategic-report-analytics.mapper.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/mappers strategic-report-analytics.mapper.ts

+
+ +
+ 100% + Statements + 8/8 +
+ + +
+ 100% + Branches + 4/4 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 8/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77  +  +  +  +7x +7x +  +7x +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +304x +277x +  +27x +  +  + 
import {
+    Prisma,
+    StrategicReportAnalytics as PrismaStrategicReportAnalytics,
+} from '@intra/database';
+import Decimal from 'decimal.js';
+import { StrategicReportAnalyticsDomain } from '../../domain/strategic-report-analytics.domain';
+ 
+export class StrategicReportAnalyticsMapper {
+    static toDomain(
+        analytics: PrismaStrategicReportAnalytics,
+    ): StrategicReportAnalyticsDomain {
+        return StrategicReportAnalyticsDomain.create({
+            id: analytics.id,
+            strategicReportId: analytics.strategicReportId,
+            competenceId: analytics.competenceId,
+            competenceTitle: analytics.competenceTitle,
+            averageBySelfAssessment: analytics.averageBySelfAssessment,
+            averageByTeam: analytics.averageByTeam,
+            averageByOther: analytics.averageByOther,
+            percentageBySelfAssessment: analytics.percentageBySelfAssessment,
+            percentageByTeam: analytics.percentageByTeam,
+            percentageByOther: analytics.percentageByOther,
+            deltaPercentageByTeam: analytics.deltaPercentageByTeam,
+            deltaPercentageByOther: analytics.deltaPercentageByOther,
+            createdAt: analytics.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        strategicReportId: number,
+        analytics: StrategicReportAnalyticsDomain,
+    ): Prisma.StrategicReportAnalyticsCreateManyInput {
+        return {
+            strategicReportId: strategicReportId,
+            competenceId: analytics.competenceId,
+            competenceTitle: analytics.competenceTitle,
+            averageBySelfAssessment:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    analytics.averageBySelfAssessment,
+                ),
+            averageByTeam: StrategicReportAnalyticsMapper.toDecimalString(
+                analytics.averageByTeam,
+            ),
+            averageByOther: StrategicReportAnalyticsMapper.toDecimalString(
+                analytics.averageByOther,
+            ),
+            percentageBySelfAssessment:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    analytics.percentageBySelfAssessment,
+                ),
+            percentageByTeam: StrategicReportAnalyticsMapper.toDecimalString(
+                analytics.percentageByTeam,
+            ),
+            percentageByOther: StrategicReportAnalyticsMapper.toDecimalString(
+                analytics.percentageByOther,
+            ),
+            deltaPercentageByTeam:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    analytics.deltaPercentageByTeam,
+                ),
+            deltaPercentageByOther:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    analytics.deltaPercentageByOther,
+                ),
+        };
+    }
+ 
+    static toDecimalString(
+        value: Decimal.Value | null | undefined,
+    ): string | undefined {
+        if (value === null || value === undefined) {
+            return undefined;
+        }
+        return new Decimal(value).toDecimalPlaces(4).toFixed(4);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report-insight.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report-insight.mapper.ts.html new file mode 100644 index 00000000..ee1c5453 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report-insight.mapper.ts.html @@ -0,0 +1,316 @@ + + + + + + Code coverage report for reporting/infrastructure/mappers/strategic-report-insight.mapper.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/mappers strategic-report-insight.mapper.ts

+
+ +
+ 83.33% + Statements + 10/12 +
+ + +
+ 100% + Branches + 4/4 +
+ + +
+ 71.42% + Functions + 5/7 +
+ + +
+ 83.33% + Lines + 10/12 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78  +  +  +  +  +  +  +7x +7x +  +7x +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +3x +  +  +  +  +  +15x +10x +  +5x +  +  + 
import {
+    Prisma,
+    EntityType as PrismaEntityType,
+    InsightType as PrismaInsightType,
+    StrategicReportInsights as PrismaStrategicReportInsights,
+} from '@intra/database';
+import { EntityType, InsightType } from '@intra/shared-kernel';
+import Decimal from 'decimal.js';
+import { StrategicReportInsightDomain } from '../../domain/startegic-report-insight.domain';
+ 
+export class StrategicReportInsightMapper {
+    static toDomain(
+        analytics: PrismaStrategicReportInsights,
+    ): StrategicReportInsightDomain {
+        return StrategicReportInsightDomain.create({
+            id: analytics.id,
+            strategicReportId: analytics.strategicReportId,
+            insightType: StrategicReportInsightMapper.toDomainInsightType(
+                analytics.insightType,
+            ),
+            competenceId: analytics.competenceId,
+            competenceTitle: analytics.competenceTitle,
+            averageScore: analytics.averageScore,
+            averageRating: analytics.averageRating,
+            averageDelta: analytics.averageDelta,
+            createdAt: analytics.createdAt,
+        });
+    }
+ 
+    static toPrisma(
+        reportId: number,
+        insights: StrategicReportInsightDomain,
+    ): Prisma.StrategicReportInsightsCreateManyInput {
+        return {
+            strategicReportId: reportId,
+            insightType: StrategicReportInsightMapper.toPrismaInsightType(
+                insights.insightType,
+            ),
+            competenceId: insights.competenceId,
+            competenceTitle: insights.competenceTitle,
+            averageScore: StrategicReportInsightMapper.toDecimalString(
+                insights.averageScore,
+            ),
+            averageRating: StrategicReportInsightMapper.toDecimalString(
+                insights.averageRating,
+            ),
+            averageDelta: StrategicReportInsightMapper.toDecimalString(
+                insights.averageDelta,
+            ),
+        };
+    }
+ 
+    static toPrismaEntityType(domainType: EntityType): PrismaEntityType {
+        return domainType.toString().toUpperCase() as PrismaEntityType;
+    }
+ 
+    static toDomainEntityType(prismaType: PrismaEntityType): EntityType {
+        return prismaType.toString().toUpperCase() as EntityType;
+    }
+ 
+    static toPrismaInsightType(domainType: InsightType): PrismaInsightType {
+        return domainType.toString().toUpperCase() as PrismaInsightType;
+    }
+ 
+    static toDomainInsightType(prismaType: PrismaInsightType): InsightType {
+        return prismaType.toString().toUpperCase() as InsightType;
+    }
+ 
+    static toDecimalString(
+        value: Decimal.Value | null | undefined,
+    ): string | undefined {
+        if (value === null || value === undefined) {
+            return undefined;
+        }
+        return new Decimal(value).toDecimalPlaces(4).toFixed(4);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report.mapper.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report.mapper.ts.html new file mode 100644 index 00000000..eb3a82f4 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/mappers/strategic-report.mapper.ts.html @@ -0,0 +1,571 @@ + + + + + + Code coverage report for reporting/infrastructure/mappers/strategic-report.mapper.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/mappers strategic-report.mapper.ts

+
+ +
+ 77.77% + Statements + 7/9 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 60% + Functions + 3/5 +
+ + +
+ 77.77% + Lines + 7/9 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163  +  +  +  +  +  +7x +7x +7x +  +7x +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +26x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +24x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    Prisma,
+    StrategicReport as PrismaStrategicReport,
+    StrategicReportAnalytics as PrismaStrategicReportAnalytics,
+    StrategicReportInsights as PrismaStrategicReportInsight,
+} from '@intra/database';
+import { StrategicReportDomain } from '../../domain/strategic-report.domain';
+import { StrategicReportAnalyticsMapper } from './strategic-report-analytics.mapper';
+import { StrategicReportInsightMapper } from './strategic-report-insight.mapper';
+ 
+export class StrategicReportMapper {
+    static toDomain(report: PrismaStrategicReport): StrategicReportDomain {
+        return StrategicReportDomain.create({
+            id: report.id,
+            cycleId: report.cycleId,
+            cycleTitle: report.cycleTitle,
+            rateeCount: report.rateeCount,
+            rateeIds: report.rateeIds,
+            respondentCount: report.respondentCount,
+            respondentIds: report.respondentIds,
+            answerCount: report.answerCount,
+            reviewerCount: report.reviewerCount,
+            reviewerIds: report.reviewerIds,
+            teamCount: report.teamCount,
+            teamIds: report.teamIds,
+            positionCount: report.positionCount,
+            positionIds: report.positionIds,
+            competenceCount: report.competenceCount,
+            competenceIds: report.competenceIds,
+            questionCount: report.questionCount,
+            questionIds: report.questionIds,
+            turnoutAvgPctOfRatees: report.turnoutAvgPctOfRatees,
+            turnoutAvgPctOfTeams: report.turnoutAvgPctOfTeams,
+            turnoutAvgPctOfOthers: report.turnoutAvgPctOfOthers,
+            competenceGeneralAvgSelf: report.competenceGeneralAvgSelf,
+            competenceGeneralAvgTeam: report.competenceGeneralAvgTeam,
+            competenceGeneralAvgOther: report.competenceGeneralAvgOther,
+            competenceGeneralPctSelf: report.competenceGeneralPctSelf,
+            competenceGeneralPctTeam: report.competenceGeneralPctTeam,
+            competenceGeneralPctOther: report.competenceGeneralPctOther,
+            competenceGeneralDeltaTeam: report.competenceGeneralDeltaTeam,
+            competenceGeneralDeltaOther: report.competenceGeneralDeltaOther,
+            createdAt: report.createdAt,
+            analytics: [],
+            insights: [],
+        });
+    }
+ 
+    static toDomainWithRelations(
+        report: PrismaStrategicReport & {
+            analytics: PrismaStrategicReportAnalytics[];
+            insights: PrismaStrategicReportInsight[];
+        },
+    ): StrategicReportDomain {
+        return StrategicReportDomain.create({
+            id: report.id,
+            cycleId: report.cycleId,
+            cycleTitle: report.cycleTitle,
+            rateeCount: report.rateeCount,
+            rateeIds: report.rateeIds,
+            respondentCount: report.respondentCount,
+            respondentIds: report.respondentIds,
+            answerCount: report.answerCount,
+            reviewerCount: report.reviewerCount,
+            reviewerIds: report.reviewerIds,
+            teamCount: report.teamCount,
+            teamIds: report.teamIds,
+            positionCount: report.positionCount,
+            positionIds: report.positionIds,
+            competenceCount: report.competenceCount,
+            competenceIds: report.competenceIds,
+            questionCount: report.questionCount,
+            questionIds: report.questionIds,
+            turnoutAvgPctOfRatees: report.turnoutAvgPctOfRatees,
+            turnoutAvgPctOfTeams: report.turnoutAvgPctOfTeams,
+            turnoutAvgPctOfOthers: report.turnoutAvgPctOfOthers,
+            competenceGeneralAvgSelf: report.competenceGeneralAvgSelf,
+            competenceGeneralAvgTeam: report.competenceGeneralAvgTeam,
+            competenceGeneralAvgOther: report.competenceGeneralAvgOther,
+            competenceGeneralPctSelf: report.competenceGeneralPctSelf,
+            competenceGeneralPctTeam: report.competenceGeneralPctTeam,
+            competenceGeneralPctOther: report.competenceGeneralPctOther,
+            competenceGeneralDeltaTeam: report.competenceGeneralDeltaTeam,
+            competenceGeneralDeltaOther: report.competenceGeneralDeltaOther,
+            createdAt: report.createdAt,
+            analytics: report.analytics.map((a) =>
+                StrategicReportAnalyticsMapper.toDomain(a),
+            ),
+            insights: report.insights.map((i) =>
+                StrategicReportInsightMapper.toDomain(i),
+            ),
+        });
+    }
+ 
+    static toPrisma(
+        domain: StrategicReportDomain,
+    ): Prisma.StrategicReportUncheckedCreateInput {
+        return {
+            cycleId: domain.cycleId,
+            cycleTitle: domain.cycleTitle,
+            rateeCount: domain.rateeCount,
+            rateeIds: domain.rateeIds,
+            respondentCount: domain.respondentCount,
+            respondentIds: domain.respondentIds,
+            answerCount: domain.answerCount,
+            reviewerCount: domain.reviewerCount,
+            reviewerIds: domain.reviewerIds,
+            teamCount: domain.teamCount,
+            teamIds: domain.teamIds,
+            positionCount: domain.positionCount,
+            positionIds: domain.positionIds,
+            competenceCount: domain.competenceCount,
+            competenceIds: domain.competenceIds,
+            questionCount: domain.questionCount,
+            questionIds: domain.questionIds,
+            turnoutAvgPctOfRatees:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.turnoutAvgPctOfRatees,
+                ),
+            turnoutAvgPctOfTeams:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.turnoutAvgPctOfTeams,
+                ),
+            turnoutAvgPctOfOthers:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.turnoutAvgPctOfOthers,
+                ),
+            competenceGeneralAvgSelf:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.competenceGeneralAvgSelf,
+                ),
+            competenceGeneralAvgTeam:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.competenceGeneralAvgTeam,
+                ),
+            competenceGeneralAvgOther:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.competenceGeneralAvgOther,
+                ),
+            competenceGeneralPctSelf:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.competenceGeneralPctSelf,
+                ),
+            competenceGeneralPctTeam:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.competenceGeneralPctTeam,
+                ),
+            competenceGeneralPctOther:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.competenceGeneralPctOther,
+                ),
+            competenceGeneralDeltaTeam:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.competenceGeneralDeltaTeam,
+                ),
+            competenceGeneralDeltaOther:
+                StrategicReportAnalyticsMapper.toDecimalString(
+                    domain.competenceGeneralDeltaOther,
+                ),
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/index.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/index.html new file mode 100644 index 00000000..a42cfb4f --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/index.html @@ -0,0 +1,206 @@ + + + + + + Code coverage report for reporting/infrastructure/prisma-repositories + + + + + + + + + +
+
+

All files reporting/infrastructure/prisma-repositories

+
+ +
+ 99.27% + Statements + 137/138 +
+ + +
+ 66.66% + Branches + 112/168 +
+ + +
+ 97.43% + Functions + 38/39 +
+ + +
+ 99.19% + Lines + 123/124 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
report-analytics.repository.ts +
+
100%18/1887.5%7/8100%5/5100%16/16
report-comment.repository.ts +
+
100%15/1583.33%5/6100%4/4100%13/13
report-insight.repository.ts +
+
100%18/1887.5%7/8100%5/5100%16/16
report.repository.ts +
+
96.15%25/2661.66%37/6087.5%7/895.83%23/24
startegic-report-insight.repository.ts +
+
100%18/1887.5%7/8100%5/5100%16/16
strategic-report-analytics.repository.ts +
+
100%18/1887.5%7/8100%5/5100%16/16
strategic-report.repository.ts +
+
100%25/2560%42/70100%7/7100%23/23
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-analytics.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-analytics.repository.ts.html new file mode 100644 index 00000000..8b48d539 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-analytics.repository.ts.html @@ -0,0 +1,226 @@ + + + + + + Code coverage report for reporting/infrastructure/prisma-repositories/report-analytics.repository.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/prisma-repositories report-analytics.repository.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 87.5% + Branches + 7/8 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +4817x +17x +17x +  +  +  +  +17x +  +  +17x +17x +  +17x +  +  +  +  +  +10x +2x +  +  +8x +11x +  +  +8x +  +  +  +6x +  +  +  +  +6x +  +  +  +4x +  +  +  +4x +  +  + 
import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    REPORT_ANALYTICS_REPOSITORY,
+    ReportAnalyticsRepositoryPort,
+} from '../../application/ports/report-analytics.repository.port';
+import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+import { ReportAnalyticsMapper } from '../mappers/report-analytics.mapper';
+ 
+@Injectable()
+export class ReportAnalyticsRepository implements ReportAnalyticsRepositoryPort {
+    readonly [REPORT_ANALYTICS_REPOSITORY] = REPORT_ANALYTICS_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async createMany(
+        reportId: number,
+        analytics: ReportAnalyticsDomain[],
+    ): Promise<void> {
+        if (analytics.length === 0) {
+            return;
+        }
+ 
+        const data = analytics.map((item) =>
+            ReportAnalyticsMapper.toPrisma(reportId, item),
+        );
+ 
+        await this.prisma.reportAnalytics.createMany({ data });
+    }
+ 
+    async findByReportId(reportId: number): Promise<ReportAnalyticsDomain[]> {
+        const analytics = await this.prisma.reportAnalytics.findMany({
+            where: { reportId },
+            orderBy: { id: 'asc' },
+        });
+ 
+        return analytics.map(ReportAnalyticsMapper.toDomain);
+    }
+ 
+    async findById(id: number): Promise<ReportAnalyticsDomain | null> {
+        const analytics = await this.prisma.reportAnalytics.findUnique({
+            where: { id },
+        });
+ 
+        return analytics ? ReportAnalyticsMapper.toDomain(analytics) : null;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-comment.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-comment.repository.ts.html new file mode 100644 index 00000000..75560677 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-comment.repository.ts.html @@ -0,0 +1,202 @@ + + + + + + Code coverage report for reporting/infrastructure/prisma-repositories/report-comment.repository.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/prisma-repositories report-comment.repository.ts

+
+ +
+ 100% + Statements + 15/15 +
+ + +
+ 83.33% + Branches + 5/6 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 13/13 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +4017x +17x +17x +  +  +  +  +17x +  +  +17x +17x +  +17x +  +  +9x +  +  +9x +  +  +  +4x +  +  +  +  +4x +  +  +  +4x +  +  +  +4x +  +  + 
import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    REPORT_COMMENT_REPOSITORY,
+    ReportCommentRepositoryPort,
+} from '../../application/ports/report-comment.repository.port';
+import { ReportCommentDomain } from '../../domain/report-comment.domain';
+import { ReportCommentMapper } from '../mappers/report-comment.mapper';
+ 
+@Injectable()
+export class ReportCommentRepository implements ReportCommentRepositoryPort {
+    readonly [REPORT_COMMENT_REPOSITORY] = REPORT_COMMENT_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async create(comment: ReportCommentDomain): Promise<ReportCommentDomain> {
+        const created = await this.prisma.reportComment.create({
+            data: ReportCommentMapper.toPrisma(comment),
+        });
+        return ReportCommentMapper.toDomain(created);
+    }
+ 
+    async findByReportId(reportId: number): Promise<ReportCommentDomain[]> {
+        const comments = await this.prisma.reportComment.findMany({
+            where: { reportId },
+            orderBy: { id: 'asc' },
+        });
+ 
+        return comments.map(ReportCommentMapper.toDomain);
+    }
+ 
+    async findById(id: number): Promise<ReportCommentDomain | null> {
+        const comment = await this.prisma.reportComment.findUnique({
+            where: { id },
+        });
+ 
+        return comment ? ReportCommentMapper.toDomain(comment) : null;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-insight.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-insight.repository.ts.html new file mode 100644 index 00000000..4ce24de4 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report-insight.repository.ts.html @@ -0,0 +1,226 @@ + + + + + + Code coverage report for reporting/infrastructure/prisma-repositories/report-insight.repository.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/prisma-repositories report-insight.repository.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 87.5% + Branches + 7/8 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +4817x +17x +17x +  +  +  +  +17x +  +  +17x +17x +  +17x +  +  +  +  +  +8x +1x +  +  +7x +23x +  +  +7x +  +  +  +4x +  +  +  +  +4x +  +  +  +4x +  +  +  +4x +  +  + 
import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    REPORT_INSIGHT_REPOSITORY,
+    ReportInsightRepositoryPort,
+} from '../../application/ports/report-insight.repository.port';
+import { ReportInsightDomain } from '../../domain/report-insight.domain';
+import { ReportInsightMapper } from '../mappers/report-insight.mapper';
+ 
+@Injectable()
+export class ReportInsightRepository implements ReportInsightRepositoryPort {
+    readonly [REPORT_INSIGHT_REPOSITORY] = REPORT_INSIGHT_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async createMany(
+        reportId: number,
+        insights: ReportInsightDomain[],
+    ): Promise<void> {
+        if (insights.length === 0) {
+            return;
+        }
+ 
+        const data = insights.map((item) =>
+            ReportInsightMapper.toPrisma(reportId, item),
+        );
+ 
+        await this.prisma.reportInsights.createMany({ data });
+    }
+ 
+    async findByReportId(reportId: number): Promise<ReportInsightDomain[]> {
+        const analytics = await this.prisma.reportInsights.findMany({
+            where: { reportId },
+            orderBy: { id: 'asc' },
+        });
+ 
+        return analytics.map(ReportInsightMapper.toDomain);
+    }
+ 
+    async findById(id: number): Promise<ReportInsightDomain | null> {
+        const analytics = await this.prisma.reportInsights.findUnique({
+            where: { id },
+        });
+ 
+        return analytics ? ReportInsightMapper.toDomain(analytics) : null;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report.repository.ts.html new file mode 100644 index 00000000..179ae71b --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/report.repository.ts.html @@ -0,0 +1,517 @@ + + + + + + Code coverage report for reporting/infrastructure/prisma-repositories/report.repository.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/prisma-repositories report.repository.ts

+
+ +
+ 96.15% + Statements + 25/26 +
+ + +
+ 61.66% + Branches + 37/60 +
+ + +
+ 87.5% + Functions + 7/8 +
+ + +
+ 95.83% + Lines + 23/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145  +17x +  +  +  +  +17x +17x +17x +  +  +  +  +17x +  +  +17x +17x +  +17x +  +  +9x +9x +9x +9x +  +  +  +59x +  +  +  +  +  +  +  +  +59x +  +  +  +20x +  +  +  +  +  +  +  +  +20x +  +  +  +10x +  +  +  +  +  +  +  +  +10x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +9x +9x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +9x +9x +9x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    ReportSearchQuery,
+    ReportSortField,
+    SortDirection,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    REPORT_REPOSITORY,
+    ReportRepositoryPort,
+} from '../../application/ports/report.repository.port';
+import { ReportDomain } from '../../domain/report.domain';
+import { ReportMapper } from '../mappers/report.mapper';
+ 
+@Injectable()
+export class ReportRepository implements ReportRepositoryPort {
+    readonly [REPORT_REPOSITORY] = REPORT_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async search(query: ReportSearchQuery): Promise<ReportDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+        const items = await this.prisma.report.findMany({ where, orderBy });
+        return items.map(ReportMapper.toDomain);
+    }
+ 
+    async create(domain: ReportDomain): Promise<ReportDomain> {
+        const created = await this.prisma.report.create({
+            data: ReportMapper.toPrisma(domain),
+            include: {
+                analytics: { orderBy: { id: 'asc' } },
+                comments: { orderBy: { id: 'asc' } },
+                insights: { orderBy: { id: 'asc' } },
+            },
+        });
+ 
+        return ReportMapper.toDomainWithRelations(created);
+    }
+ 
+    async findById(id: number): Promise<ReportDomain | null> {
+        const report = await this.prisma.report.findUnique({
+            where: { id },
+            include: {
+                analytics: { orderBy: { id: 'asc' } },
+                comments: { orderBy: { id: 'asc' } },
+                insights: { orderBy: { id: 'asc' } },
+            },
+        });
+ 
+        return report ? ReportMapper.toDomainWithRelations(report) : null;
+    }
+ 
+    async findByReviewId(reviewId: number): Promise<ReportDomain | null> {
+        const report = await this.prisma.report.findUnique({
+            where: { reviewId },
+            include: {
+                analytics: { orderBy: { id: 'asc' } },
+                comments: { orderBy: { id: 'asc' } },
+                insights: { orderBy: { id: 'asc' } },
+            },
+        });
+ 
+        return report ? ReportMapper.toDomainWithRelations(report) : null;
+    }
+ 
+    private buildWhere(query: ReportSearchQuery): Prisma.ReportWhereInput {
+        const {
+            reviewId,
+            cycleId,
+            respondentCount,
+            respondentCategories,
+            answerCount,
+            turnoutPctOfTeam,
+            turnoutPctOfOther,
+            questionTotAvgBySelf,
+            questionTotAvgByTeam,
+            questionTotAvgByOthers,
+            questionTotPctBySelf,
+            questionTotPctByTeam,
+            questionTotPctByOthers,
+            questionTotDeltaPctByTeam,
+            questionTotDeltaPctByOthers,
+            competenceTotAvgBySelf,
+            competenceTotAvgByTeam,
+            competenceTotAvgByOthers,
+            competenceTotPctBySelf,
+            competenceTotPctByTeam,
+            competenceTotPctByOthers,
+            competenceTotDeltaPctByTeam,
+            competenceTotDeltaPctByOthers,
+            createdAt,
+        } = query;
+        return {
+            ...(reviewId ? { reviewId } : {}),
+            ...(cycleId ? { cycleId } : {}),
+            ...(respondentCount ? { respondentCount } : {}),
+            ...(respondentCategories
+                ? {
+                      respondentCategories: {
+                          hasSome: respondentCategories.map((c) =>
+                              ReportMapper.toPrismaRespondentCategory(c),
+                          ),
+                      },
+                  }
+                : {}),
+            ...(answerCount ? { answerCount } : {}),
+            ...(turnoutPctOfTeam ? { turnoutPctOfTeam } : {}),
+            ...(turnoutPctOfOther ? { turnoutPctOfOther } : {}),
+            ...(questionTotAvgBySelf ? { questionTotAvgBySelf } : {}),
+            ...(questionTotAvgByTeam ? { questionTotAvgByTeam } : {}),
+            ...(questionTotAvgByOthers ? { questionTotAvgByOthers } : {}),
+            ...(questionTotPctBySelf ? { questionTotPctBySelf } : {}),
+            ...(questionTotPctByTeam ? { questionTotPctByTeam } : {}),
+            ...(questionTotPctByOthers ? { questionTotPctByOthers } : {}),
+            ...(questionTotDeltaPctByTeam ? { questionTotDeltaPctByTeam } : {}),
+            ...(questionTotDeltaPctByOthers
+                ? { questionTotDeltaPctByOthers }
+                : {}),
+            ...(competenceTotAvgBySelf ? { competenceTotAvgBySelf } : {}),
+            ...(competenceTotAvgByTeam ? { competenceTotAvgByTeam } : {}),
+            ...(competenceTotAvgByOthers ? { competenceTotAvgByOthers } : {}),
+            ...(competenceTotPctBySelf ? { competenceTotPctBySelf } : {}),
+            ...(competenceTotPctByTeam ? { competenceTotPctByTeam } : {}),
+            ...(competenceTotPctByOthers ? { competenceTotPctByOthers } : {}),
+            ...(competenceTotDeltaPctByTeam
+                ? { competenceTotDeltaPctByTeam }
+                : {}),
+            ...(competenceTotDeltaPctByOthers
+                ? { competenceTotDeltaPctByOthers }
+                : {}),
+            ...(createdAt ? { createdAt } : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: ReportSearchQuery,
+    ): Prisma.ReportOrderByWithRelationInput[] {
+        const field = query.sortBy ?? ReportSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository.ts.html new file mode 100644 index 00000000..92d8508b --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository.ts.html @@ -0,0 +1,241 @@ + + + + + + Code coverage report for reporting/infrastructure/prisma-repositories/startegic-report-insight.repository.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/prisma-repositories startegic-report-insight.repository.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 87.5% + Branches + 7/8 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +5317x +17x +17x +  +  +  +  +17x +  +  +17x +  +17x +  +17x +  +  +  +  +  +8x +1x +  +  +7x +15x +  +  +7x +  +  +  +  +  +4x +  +  +  +  +4x +  +  +  +4x +  +  +  +4x +  +  +  +  + 
import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    STRATEGIC_REPORT_INSIGHT_REPOSITORY,
+    StrategicReportInsightRepositoryPort,
+} from '../../application/ports/strategic-report-insight.repository.port';
+import { StrategicReportInsightDomain } from '../../domain/startegic-report-insight.domain';
+import { StrategicReportInsightMapper } from '../mappers/strategic-report-insight.mapper';
+ 
+@Injectable()
+export class StrategicReportInsightRepository implements StrategicReportInsightRepositoryPort {
+    readonly [STRATEGIC_REPORT_INSIGHT_REPOSITORY] =
+        STRATEGIC_REPORT_INSIGHT_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async createMany(
+        strategicReportId: number,
+        insights: StrategicReportInsightDomain[],
+    ): Promise<void> {
+        if (insights.length === 0) {
+            return;
+        }
+ 
+        const data = insights.map((item) =>
+            StrategicReportInsightMapper.toPrisma(strategicReportId, item),
+        );
+ 
+        await this.prisma.strategicReportInsights.createMany({ data });
+    }
+ 
+    async findByStrategicReportId(
+        reportId: number,
+    ): Promise<StrategicReportInsightDomain[]> {
+        const analytics = await this.prisma.strategicReportInsights.findMany({
+            where: { strategicReportId: reportId },
+            orderBy: { id: 'asc' },
+        });
+ 
+        return analytics.map(StrategicReportInsightMapper.toDomain);
+    }
+ 
+    async findById(id: number): Promise<StrategicReportInsightDomain | null> {
+        const analytics = await this.prisma.strategicReportInsights.findUnique({
+            where: { id },
+        });
+ 
+        return analytics
+            ? StrategicReportInsightMapper.toDomain(analytics)
+            : null;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository.ts.html new file mode 100644 index 00000000..b3215391 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository.ts.html @@ -0,0 +1,247 @@ + + + + + + Code coverage report for reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/prisma-repositories strategic-report-analytics.repository.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 87.5% + Branches + 7/8 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +5517x +17x +17x +  +  +  +  +17x +  +  +17x +  +17x +  +17x +  +  +  +  +  +8x +1x +  +  +7x +9x +  +  +7x +  +  +  +  +  +4x +  +  +  +  +4x +  +  +  +4x +  +  +  +  +  +4x +  +  +  +  + 
import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    STRATEGIC_REPORT_ANALYTICS_REPOSITORY,
+    StrategicReportAnalyticsRepositoryPort,
+} from '../../application/ports/strategic-report-analytics.repository.port';
+import { StrategicReportAnalyticsDomain } from '../../domain/strategic-report-analytics.domain';
+import { StrategicReportAnalyticsMapper } from '../mappers/strategic-report-analytics.mapper';
+ 
+@Injectable()
+export class StrategicReportAnalyticsRepository implements StrategicReportAnalyticsRepositoryPort {
+    readonly [STRATEGIC_REPORT_ANALYTICS_REPOSITORY] =
+        STRATEGIC_REPORT_ANALYTICS_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async createMany(
+        reportId: number,
+        analytics: StrategicReportAnalyticsDomain[],
+    ): Promise<void> {
+        if (analytics.length === 0) {
+            return;
+        }
+ 
+        const data = analytics.map((item) =>
+            StrategicReportAnalyticsMapper.toPrisma(reportId, item),
+        );
+ 
+        await this.prisma.strategicReportAnalytics.createMany({ data });
+    }
+ 
+    async findByStrategicReportId(
+        strategicReportId: number,
+    ): Promise<StrategicReportAnalyticsDomain[]> {
+        const analytics = await this.prisma.strategicReportAnalytics.findMany({
+            where: { strategicReportId },
+            orderBy: { id: 'asc' },
+        });
+ 
+        return analytics.map(StrategicReportAnalyticsMapper.toDomain);
+    }
+ 
+    async findById(id: number): Promise<StrategicReportAnalyticsDomain | null> {
+        const analytics = await this.prisma.strategicReportAnalytics.findUnique(
+            {
+                where: { id },
+            },
+        );
+ 
+        return analytics
+            ? StrategicReportAnalyticsMapper.toDomain(analytics)
+            : null;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/strategic-report.repository.ts.html b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/strategic-report.repository.ts.html new file mode 100644 index 00000000..07734cec --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/reporting/infrastructure/prisma-repositories/strategic-report.repository.ts.html @@ -0,0 +1,568 @@ + + + + + + Code coverage report for reporting/infrastructure/prisma-repositories/strategic-report.repository.ts + + + + + + + + + +
+
+

All files / reporting/infrastructure/prisma-repositories strategic-report.repository.ts

+
+ +
+ 100% + Statements + 25/25 +
+ + +
+ 60% + Branches + 42/70 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 23/23 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162  +17x +  +  +  +  +17x +17x +17x +  +  +  +  +17x +  +  +17x +17x +  +17x +  +  +  +  +5x +5x +5x +  +  +  +5x +  +  +  +  +  +42x +  +  +  +  +  +  +  +42x +  +  +  +16x +  +  +  +  +  +  +  +16x +  +  +  +  +  +  +  +11x +  +  +  +  +  +  +  +  +11x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +5x +5x +  +  + 
import { Prisma } from '@intra/database';
+import {
+    SortDirection,
+    StrategicReportSearchQuery,
+    StrategicReportSortField,
+} from '@intra/shared-kernel';
+import { Injectable } from '@nestjs/common';
+import { PrismaService } from 'src/database/prisma.service';
+import {
+    STRATEGIC_REPORT_REPOSITORY,
+    StrategicReportRepositoryPort,
+} from '../../application/ports/strategic-report.repository.port';
+import { StrategicReportDomain } from '../../domain/strategic-report.domain';
+import { StrategicReportMapper } from '../mappers/strategic-report.mapper';
+ 
+@Injectable()
+export class StrategicReportRepository implements StrategicReportRepositoryPort {
+    readonly [STRATEGIC_REPORT_REPOSITORY] = STRATEGIC_REPORT_REPOSITORY;
+ 
+    constructor(private readonly prisma: PrismaService) {}
+ 
+    async search(
+        query: StrategicReportSearchQuery,
+    ): Promise<StrategicReportDomain[]> {
+        const where = this.buildWhere(query);
+        const orderBy = this.buildOrder(query);
+        const items = await this.prisma.strategicReport.findMany({
+            where,
+            orderBy,
+        });
+        return items.map(StrategicReportMapper.toDomain);
+    }
+ 
+    async create(
+        domain: StrategicReportDomain,
+    ): Promise<StrategicReportDomain> {
+        const created = await this.prisma.strategicReport.create({
+            data: StrategicReportMapper.toPrisma(domain),
+            include: {
+                analytics: { orderBy: { id: 'asc' } },
+                insights: { orderBy: { id: 'asc' } },
+            },
+        });
+ 
+        return StrategicReportMapper.toDomainWithRelations(created);
+    }
+ 
+    async findById(id: number): Promise<StrategicReportDomain | null> {
+        const report = await this.prisma.strategicReport.findUnique({
+            where: { id },
+            include: {
+                analytics: { orderBy: { id: 'asc' } },
+                insights: { orderBy: { id: 'asc' } },
+            },
+        });
+ 
+        return report
+            ? StrategicReportMapper.toDomainWithRelations(report)
+            : null;
+    }
+ 
+    async findByCycleId(
+        cycleId: number,
+    ): Promise<StrategicReportDomain | null> {
+        const report = await this.prisma.strategicReport.findFirst({
+            where: { cycleId },
+            include: {
+                analytics: { orderBy: { id: 'asc' } },
+                insights: { orderBy: { id: 'asc' } },
+            },
+            orderBy: { createdAt: 'desc' },
+        });
+ 
+        return report
+            ? StrategicReportMapper.toDomainWithRelations(report)
+            : null;
+    }
+ 
+    private buildWhere(
+        query: StrategicReportSearchQuery,
+    ): Prisma.StrategicReportWhereInput {
+        const {
+            cycleId,
+            cycleTitle,
+            rateeCount,
+            rateeIds,
+            respondentCount,
+            respondentIds,
+            answerCount,
+            reviewerCount,
+            reviewerIds,
+            teamCount,
+            teamIds,
+            positionCount,
+            positionIds,
+            competenceCount,
+            competenceIds,
+            questionCount,
+            questionIds,
+            turnoutAvgPctOfRatees,
+            turnoutAvgPctOfTeams,
+            turnoutAvgPctOfOthers,
+            competenceGeneralAvgSelf,
+            competenceGeneralAvgTeam,
+            competenceGeneralAvgOther,
+            competenceGeneralPctSelf,
+            competenceGeneralPctTeam,
+            competenceGeneralPctOther,
+            competenceGeneralDeltaTeam,
+            competenceGeneralDeltaOther,
+            createdAt,
+        } = query;
+        return {
+            ...(cycleId ? { cycleId } : {}),
+            ...(cycleTitle ? { cycleTitle } : {}),
+            ...(rateeIds ? { rateeIds: { hasSome: rateeIds } } : {}),
+            ...(rateeCount ? { rateeCount } : {}),
+            ...(respondentIds
+                ? { respondentIds: { hasSome: respondentIds } }
+                : {}),
+            ...(respondentCount ? { respondentCount } : {}),
+            ...(answerCount ? { answerCount } : {}),
+            ...(reviewerIds ? { reviewerIds: { hasSome: reviewerIds } } : {}),
+            ...(reviewerCount ? { reviewerCount } : {}),
+            ...(teamIds ? { teamIds: { hasSome: teamIds } } : {}),
+            ...(teamCount ? { teamCount } : {}),
+            ...(positionIds ? { positionIds: { hasSome: positionIds } } : {}),
+            ...(positionCount ? { positionCount } : {}),
+            ...(competenceIds
+                ? { competenceIds: { hasSome: competenceIds } }
+                : {}),
+            ...(competenceCount ? { competenceCount } : {}),
+            ...(questionIds ? { questionIds: { hasSome: questionIds } } : {}),
+            ...(questionCount ? { questionCount } : {}),
+            ...(turnoutAvgPctOfRatees ? { turnoutAvgPctOfRatees } : {}),
+            ...(turnoutAvgPctOfTeams ? { turnoutAvgPctOfTeams } : {}),
+            ...(turnoutAvgPctOfOthers ? { turnoutAvgPctOfOthers } : {}),
+            ...(createdAt ? { createdAt } : {}),
+            ...(competenceGeneralAvgSelf ? { competenceGeneralAvgSelf } : {}),
+            ...(competenceGeneralAvgTeam ? { competenceGeneralAvgTeam } : {}),
+            ...(competenceGeneralAvgOther ? { competenceGeneralAvgOther } : {}),
+            ...(competenceGeneralPctSelf ? { competenceGeneralPctSelf } : {}),
+            ...(competenceGeneralPctTeam ? { competenceGeneralPctTeam } : {}),
+            ...(competenceGeneralPctOther ? { competenceGeneralPctOther } : {}),
+            ...(competenceGeneralDeltaTeam
+                ? { competenceGeneralDeltaTeam }
+                : {}),
+            ...(competenceGeneralDeltaOther
+                ? { competenceGeneralDeltaOther }
+                : {}),
+        };
+    }
+ 
+    private buildOrder(
+        query: StrategicReportSearchQuery,
+    ): Prisma.StrategicReportOrderByWithRelationInput[] {
+        const field = query.sortBy ?? StrategicReportSortField.ID;
+        const direction = query.sortDirection ?? SortDirection.ASC;
+        return [{ [field]: direction.toLowerCase() as Prisma.SortOrder }];
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/integration/coverage/lcov-report/sort-arrow-sprite.png b/apps/api/test/integration/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 00000000..6ed68316 Binary files /dev/null and b/apps/api/test/integration/coverage/lcov-report/sort-arrow-sprite.png differ diff --git a/apps/api/test/integration/coverage/lcov-report/sorter.js b/apps/api/test/integration/coverage/lcov-report/sorter.js new file mode 100644 index 00000000..4ed70ae5 --- /dev/null +++ b/apps/api/test/integration/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/apps/api/test/integration/coverage/lcov.info b/apps/api/test/integration/coverage/lcov.info new file mode 100644 index 00000000..87d56c7e --- /dev/null +++ b/apps/api/test/integration/coverage/lcov.info @@ -0,0 +1,6330 @@ +TN: +SF:src/contexts/feedback360/application/listeners/cycle-stage.listener.ts +FN:21,(anonymous_4) +FN:34,(anonymous_5) +FNF:2 +FNH:2 +FNDA:18,(anonymous_4) +FNDA:14,(anonymous_5) +DA:1,18 +DA:2,18 +DA:3,18 +DA:4,18 +DA:5,18 +DA:6,18 +DA:7,18 +DA:18,18 +DA:19,18 +DA:22,18 +DA:23,18 +DA:25,18 +DA:34,18 +DA:37,14 +DA:41,14 +DA:43,8 +DA:47,8 +DA:50,6 +DA:52,6 +DA:53,2 +DA:56,2 +DA:60,2 +DA:63,4 +DA:67,4 +DA:68,4 +DA:70,4 +DA:74,0 +DA:79,4 +LF:28 +LH:27 +BRDA:41,0,0,8 +BRDA:41,0,1,6 +BRDA:52,1,0,2 +BRDA:52,1,1,4 +BRDA:35,2,0,18 +BRDA:35,2,1,0 +BRDA:35,3,0,18 +BRDA:35,3,1,18 +BRDA:36,4,0,18 +BRDA:36,4,1,0 +BRDA:36,5,0,18 +BRDA:36,5,1,18 +BRDA:22,6,0,18 +BRDA:22,6,1,0 +BRDA:22,7,0,18 +BRDA:22,7,1,18 +BRDA:22,8,0,18 +BRDA:22,8,1,0 +BRDA:22,9,0,18 +BRDA:22,9,1,18 +BRDA:23,10,0,0 +BRDA:23,10,1,18 +BRDA:23,11,0,18 +BRDA:23,11,1,0 +BRF:24 +BRH:18 +end_of_record +TN: +SF:src/contexts/feedback360/application/listeners/respondent-status.listener.ts +FN:28,(anonymous_4) +FN:41,(anonymous_5) +FN:65,(anonymous_6) +FN:79,(anonymous_7) +FN:109,(anonymous_8) +FN:118,(anonymous_9) +FNF:6 +FNH:6 +FNDA:18,(anonymous_4) +FNDA:5,(anonymous_5) +FNDA:4,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:18,(anonymous_8) +FNDA:6,(anonymous_9) +DA:1,18 +DA:6,18 +DA:7,18 +DA:8,18 +DA:9,18 +DA:10,18 +DA:14,18 +DA:18,18 +DA:25,18 +DA:26,18 +DA:30,18 +DA:31,18 +DA:32,18 +DA:41,18 +DA:44,5 +DA:48,5 +DA:52,2 +DA:55,3 +DA:59,3 +DA:60,3 +DA:64,3 +DA:66,4 +DA:70,3 +DA:71,2 +DA:72,2 +DA:76,1 +DA:80,2 +DA:89,0 +DA:106,18 +DA:107,18 +DA:111,18 +DA:113,18 +DA:114,18 +DA:118,18 +DA:121,6 +DA:122,2 +DA:125,4 +DA:126,4 +DA:130,4 +DA:131,2 +DA:134,2 +DA:135,2 +DA:136,0 +DA:139,0 +DA:142,2 +DA:143,1 +DA:146,1 +DA:150,1 +DA:158,0 +LF:49 +LH:45 +BRDA:48,0,0,2 +BRDA:48,0,1,3 +BRDA:49,1,0,5 +BRDA:49,1,1,2 +BRDA:66,2,0,4 +BRDA:66,2,1,1 +BRDA:70,3,0,2 +BRDA:70,3,1,1 +BRDA:80,4,0,2 +BRDA:80,4,1,1 +BRDA:42,5,0,18 +BRDA:42,5,1,0 +BRDA:42,6,0,18 +BRDA:42,6,1,18 +BRDA:43,7,0,18 +BRDA:43,7,1,0 +BRDA:43,8,0,18 +BRDA:43,8,1,18 +BRDA:30,9,0,0 +BRDA:30,9,1,18 +BRDA:30,10,0,18 +BRDA:30,10,1,0 +BRDA:30,11,0,18 +BRDA:30,11,1,0 +BRDA:30,12,0,18 +BRDA:30,12,1,18 +BRDA:31,13,0,18 +BRDA:31,13,1,0 +BRDA:31,14,0,18 +BRDA:31,14,1,18 +BRDA:121,15,0,2 +BRDA:121,15,1,4 +BRDA:130,16,0,2 +BRDA:130,16,1,2 +BRDA:135,17,0,0 +BRDA:135,17,1,2 +BRDA:142,18,0,1 +BRDA:142,18,1,1 +BRDA:153,19,0,1 +BRDA:153,19,1,1 +BRDA:154,20,0,1 +BRDA:154,20,1,1 +BRDA:119,21,0,18 +BRDA:119,21,1,0 +BRDA:119,22,0,18 +BRDA:119,22,1,18 +BRDA:120,23,0,18 +BRDA:120,23,1,0 +BRDA:120,24,0,18 +BRDA:120,24,1,18 +BRDA:111,25,0,0 +BRDA:111,25,1,18 +BRDA:111,26,0,18 +BRDA:111,26,1,0 +BRDA:111,27,0,0 +BRDA:111,27,1,18 +BRDA:111,28,0,18 +BRDA:111,28,1,0 +BRDA:113,29,0,18 +BRDA:113,29,1,0 +BRDA:113,30,0,18 +BRDA:113,30,1,18 +BRF:62 +BRH:48 +end_of_record +TN: +SF:src/contexts/feedback360/application/listeners/review-stage.listener.ts +FN:35,(anonymous_4) +FN:48,(anonymous_5) +FN:92,(anonymous_6) +FNF:3 +FNH:3 +FNDA:18,(anonymous_4) +FNDA:11,(anonymous_5) +FNDA:7,(anonymous_6) +DA:1,18 +DA:2,18 +DA:3,18 +DA:4,18 +DA:5,18 +DA:6,18 +DA:7,18 +DA:20,18 +DA:32,18 +DA:33,18 +DA:36,18 +DA:38,18 +DA:39,18 +DA:48,18 +DA:51,11 +DA:55,11 +DA:56,11 +DA:57,3 +DA:60,8 +DA:62,8 +DA:63,0 +DA:64,0 +DA:67,8 +DA:68,1 +DA:71,1 +DA:74,7 +DA:78,7 +DA:79,7 +DA:92,6 +DA:93,7 +DA:95,6 +DA:97,6 +DA:98,5 +DA:99,1 +DA:103,1 +DA:108,4 +DA:119,10 +LF:37 +LH:35 +BRDA:56,0,0,3 +BRDA:56,0,1,8 +BRDA:62,1,0,0 +BRDA:62,1,1,8 +BRDA:67,2,0,1 +BRDA:67,2,1,7 +BRDA:97,3,0,5 +BRDA:97,3,1,1 +BRDA:49,4,0,18 +BRDA:49,4,1,0 +BRDA:49,5,0,18 +BRDA:49,5,1,18 +BRDA:50,6,0,18 +BRDA:50,6,1,0 +BRDA:50,7,0,18 +BRDA:50,7,1,18 +BRDA:36,8,0,18 +BRDA:36,8,1,0 +BRDA:36,9,0,18 +BRDA:36,9,1,18 +BRDA:36,10,0,0 +BRDA:36,10,1,18 +BRDA:36,11,0,18 +BRDA:36,11,1,0 +BRDA:38,12,0,18 +BRDA:38,12,1,0 +BRDA:38,13,0,18 +BRDA:38,13,1,18 +BRF:28 +BRH:21 +end_of_record +TN: +SF:src/contexts/feedback360/application/services/cluster-score-analytics.service.ts +FN:28,(anonymous_5) +FN:37,(anonymous_6) +FN:65,(anonymous_7) +FN:71,(anonymous_8) +FN:80,(anonymous_9) +FN:122,(anonymous_10) +FN:127,(anonymous_11) +FN:137,(anonymous_12) +FN:151,(anonymous_13) +FN:159,(anonymous_14) +FN:164,(anonymous_15) +FN:170,(anonymous_16) +FN:171,(anonymous_17) +FN:174,(anonymous_18) +FN:175,(anonymous_19) +FN:209,(anonymous_20) +FN:231,(anonymous_21) +FNF:17 +FNH:17 +FNDA:35,(anonymous_5) +FNDA:13,(anonymous_6) +FNDA:1,(anonymous_7) +FNDA:6,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:1,(anonymous_10) +FNDA:6,(anonymous_11) +FNDA:7,(anonymous_12) +FNDA:3,(anonymous_13) +FNDA:3,(anonymous_14) +FNDA:3,(anonymous_15) +FNDA:2,(anonymous_16) +FNDA:2,(anonymous_17) +FNDA:3,(anonymous_18) +FNDA:3,(anonymous_19) +FNDA:15,(anonymous_20) +FNDA:14,(anonymous_21) +DA:1,35 +DA:7,35 +DA:13,35 +DA:14,35 +DA:15,35 +DA:16,35 +DA:20,35 +DA:24,35 +DA:27,35 +DA:30,35 +DA:32,35 +DA:33,35 +DA:34,35 +DA:40,13 +DA:41,12 +DA:42,12 +DA:43,12 +DA:44,12 +DA:45,12 +DA:47,12 +DA:48,11 +DA:50,9 +DA:62,9 +DA:68,1 +DA:72,6 +DA:73,6 +DA:74,2 +DA:77,4 +DA:84,2 +DA:87,2 +DA:91,2 +DA:95,2 +DA:99,2 +DA:101,1 +DA:119,1 +DA:123,1 +DA:124,1 +DA:130,6 +DA:138,7 +DA:140,6 +DA:141,6 +DA:145,6 +DA:146,3 +DA:147,0 +DA:150,3 +DA:151,3 +DA:154,3 +DA:155,1 +DA:158,2 +DA:159,3 +DA:161,2 +DA:162,2 +DA:163,2 +DA:164,3 +DA:167,2 +DA:169,2 +DA:170,2 +DA:171,2 +DA:172,2 +DA:174,3 +DA:175,3 +DA:177,2 +DA:181,2 +DA:182,2 +DA:184,2 +DA:185,2 +DA:187,2 +DA:199,2 +DA:214,15 +DA:215,1 +DA:219,14 +DA:220,2 +DA:235,14 +DA:236,0 +DA:240,14 +DA:241,0 +DA:245,14 +DA:246,1 +LF:78 +LH:75 +BRDA:73,0,0,2 +BRDA:73,0,1,4 +BRDA:87,1,0,0 +BRDA:87,1,1,2 +BRDA:91,2,0,0 +BRDA:91,2,1,2 +BRDA:95,3,0,1 +BRDA:95,3,1,1 +BRDA:102,4,0,1 +BRDA:102,4,1,0 +BRDA:105,5,0,0 +BRDA:105,5,1,1 +BRDA:108,6,0,0 +BRDA:108,6,1,1 +BRDA:111,7,0,0 +BRDA:111,7,1,1 +BRDA:114,8,0,0 +BRDA:114,8,1,1 +BRDA:146,9,0,0 +BRDA:146,9,1,3 +BRDA:154,10,0,1 +BRDA:154,10,1,2 +BRDA:214,11,0,1 +BRDA:214,11,1,14 +BRDA:219,12,0,2 +BRDA:219,12,1,12 +BRDA:219,13,0,14 +BRDA:219,13,1,14 +BRDA:235,14,0,0 +BRDA:235,14,1,14 +BRDA:240,15,0,0 +BRDA:240,15,1,14 +BRDA:245,16,0,1 +BRDA:245,16,1,13 +BRDA:30,17,0,0 +BRDA:30,17,1,35 +BRDA:30,18,0,35 +BRDA:30,18,1,0 +BRDA:30,19,0,0 +BRDA:30,19,1,35 +BRDA:30,20,0,35 +BRDA:30,20,1,0 +BRDA:32,21,0,35 +BRDA:32,21,1,0 +BRDA:32,22,0,35 +BRDA:32,22,1,35 +BRDA:33,23,0,35 +BRDA:33,23,1,0 +BRDA:33,24,0,35 +BRDA:33,24,1,35 +BRF:50 +BRH:34 +end_of_record +TN: +SF:src/contexts/feedback360/application/services/cycle.service.ts +FN:44,(anonymous_4) +FN:57,(anonymous_5) +FN:78,(anonymous_6) +FN:82,(anonymous_7) +FN:89,(anonymous_8) +FN:133,(anonymous_9) +FN:182,(anonymous_10) +FN:196,(anonymous_11) +FN:216,(anonymous_12) +FN:253,(anonymous_13) +FN:262,(anonymous_14) +FN:270,(anonymous_15) +FN:299,(anonymous_16) +FNF:13 +FNH:13 +FNDA:37,(anonymous_4) +FNDA:60,(anonymous_5) +FNDA:7,(anonymous_6) +FNDA:151,(anonymous_7) +FNDA:5,(anonymous_8) +FNDA:1,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:16,(anonymous_11) +FNDA:13,(anonymous_12) +FNDA:2,(anonymous_13) +FNDA:8,(anonymous_14) +FNDA:5,(anonymous_15) +FNDA:2,(anonymous_16) +DA:1,38 +DA:10,38 +DA:16,38 +DA:17,38 +DA:21,38 +DA:22,38 +DA:23,38 +DA:24,38 +DA:28,38 +DA:29,38 +DA:33,38 +DA:37,38 +DA:43,38 +DA:46,37 +DA:48,37 +DA:50,37 +DA:51,37 +DA:52,37 +DA:54,37 +DA:58,60 +DA:74,60 +DA:75,60 +DA:79,7 +DA:83,151 +DA:84,151 +DA:85,9 +DA:86,142 +DA:90,5 +DA:92,4 +DA:117,4 +DA:119,4 +DA:120,0 +DA:128,4 +DA:130,4 +DA:132,3 +DA:133,3 +DA:134,1 +DA:140,3 +DA:142,3 +DA:153,1 +DA:164,2 +DA:165,0 +DA:168,2 +DA:169,2 +DA:179,3 +DA:183,2 +DA:185,1 +DA:203,16 +DA:204,15 +DA:207,15 +DA:209,15 +DA:210,2 +DA:216,13 +DA:218,13 +DA:224,13 +DA:233,13 +DA:237,13 +DA:254,2 +DA:263,8 +DA:266,6 +DA:269,6 +DA:271,5 +DA:274,6 +DA:277,6 +DA:278,4 +DA:285,2 +DA:286,1 +DA:304,2 +DA:305,2 +LF:69 +LH:67 +BRDA:62,0,0,60 +BRDA:62,0,1,60 +BRDA:64,1,0,60 +BRDA:64,1,1,60 +BRDA:66,2,0,60 +BRDA:66,2,1,60 +BRDA:84,3,0,9 +BRDA:84,3,1,142 +BRDA:93,4,0,0 +BRDA:93,4,1,4 +BRDA:94,5,0,1 +BRDA:94,5,1,3 +BRDA:97,6,0,0 +BRDA:97,6,1,4 +BRDA:98,7,0,0 +BRDA:98,7,1,4 +BRDA:101,8,0,0 +BRDA:101,8,1,4 +BRDA:104,9,0,0 +BRDA:104,9,1,4 +BRDA:107,10,0,0 +BRDA:107,10,1,4 +BRDA:110,11,0,0 +BRDA:110,11,1,4 +BRDA:113,12,0,0 +BRDA:113,12,1,4 +BRDA:119,13,0,0 +BRDA:119,13,1,4 +BRDA:119,14,0,4 +BRDA:119,14,1,0 +BRDA:130,15,0,3 +BRDA:130,15,1,1 +BRDA:130,16,0,4 +BRDA:130,16,1,3 +BRDA:142,17,0,1 +BRDA:142,17,1,2 +BRDA:143,18,0,3 +BRDA:143,18,1,1 +BRDA:143,18,2,1 +BRDA:164,19,0,0 +BRDA:164,19,1,2 +BRDA:164,20,0,2 +BRDA:164,20,1,2 +BRDA:168,21,0,2 +BRDA:168,21,1,0 +BRDA:168,22,0,2 +BRDA:168,22,1,2 +BRDA:199,23,0,7 +BRDA:200,24,0,7 +BRDA:209,25,0,2 +BRDA:209,25,1,13 +BRDA:228,26,0,13 +BRDA:228,26,1,13 +BRDA:271,27,0,5 +BRDA:271,27,1,5 +BRDA:277,28,0,4 +BRDA:277,28,1,2 +BRDA:277,29,0,6 +BRDA:277,29,1,5 +BRDA:285,30,0,1 +BRDA:285,30,1,1 +BRDA:285,31,0,2 +BRDA:285,31,1,1 +BRDA:301,32,0,2 +BRDA:302,33,0,2 +BRDA:46,34,0,0 +BRDA:46,34,1,38 +BRDA:46,35,0,38 +BRDA:46,35,1,0 +BRDA:46,36,0,0 +BRDA:46,36,1,38 +BRDA:46,37,0,38 +BRDA:46,37,1,0 +BRDA:48,38,0,0 +BRDA:48,38,1,38 +BRDA:48,39,0,38 +BRDA:48,39,1,0 +BRDA:50,40,0,38 +BRDA:50,40,1,0 +BRDA:50,41,0,38 +BRDA:50,41,1,38 +BRDA:51,42,0,38 +BRDA:51,42,1,0 +BRDA:51,43,0,38 +BRDA:51,43,1,38 +BRDA:52,44,0,0 +BRDA:52,44,1,38 +BRDA:52,45,0,38 +BRDA:52,45,1,0 +BRF:89 +BRH:67 +end_of_record +TN: +SF:src/contexts/feedback360/application/services/review-scheduler.service.ts +FN:38,(anonymous_2) +FN:48,(anonymous_3) +FN:113,(anonymous_4) +FNF:3 +FNH:3 +FNDA:18,(anonymous_2) +FNDA:4,(anonymous_3) +FNDA:2,(anonymous_4) +DA:1,18 +DA:7,18 +DA:8,18 +DA:9,18 +DA:10,18 +DA:11,18 +DA:12,18 +DA:18,18 +DA:35,18 +DA:36,18 +DA:39,18 +DA:40,18 +DA:48,18 +DA:49,4 +DA:51,4 +DA:53,4 +DA:57,4 +DA:58,4 +DA:59,1 +DA:62,3 +DA:63,3 +DA:66,3 +DA:68,2 +DA:76,2 +DA:81,2 +DA:82,1 +DA:83,1 +DA:90,1 +DA:94,0 +DA:102,0 +DA:113,18 +DA:114,2 +DA:116,2 +DA:117,2 +DA:122,2 +DA:124,2 +DA:126,2 +DA:128,2 +DA:131,2 +DA:132,1 +DA:135,1 +DA:136,1 +DA:137,1 +DA:141,0 +DA:147,0 +LF:45 +LH:41 +BRDA:58,0,0,1 +BRDA:58,0,1,3 +BRDA:66,1,0,2 +BRDA:66,1,1,1 +BRDA:126,2,0,2 +BRDA:126,2,1,2 +BRDA:128,3,0,2 +BRDA:128,3,1,1 +BRDA:131,4,0,1 +BRDA:131,4,1,1 +BRDA:131,5,0,2 +BRDA:131,5,1,1 +BRDA:48,6,0,18 +BRDA:48,6,1,0 +BRDA:48,7,0,18 +BRDA:48,7,1,18 +BRDA:113,8,0,18 +BRDA:113,8,1,0 +BRDA:113,9,0,18 +BRDA:113,9,1,18 +BRDA:39,10,0,18 +BRDA:39,10,1,0 +BRDA:39,11,0,18 +BRDA:39,11,1,18 +BRDA:39,12,0,18 +BRDA:39,12,1,0 +BRDA:39,13,0,18 +BRDA:39,13,1,18 +BRF:28 +BRH:24 +end_of_record +TN: +SF:src/contexts/feedback360/application/services/review.service.ts +FN:91,(anonymous_4) +FN:115,(anonymous_5) +FN:143,(anonymous_6) +FN:151,(anonymous_7) +FN:159,(anonymous_8) +FN:164,(anonymous_9) +FN:180,(anonymous_10) +FN:208,(anonymous_11) +FN:246,(anonymous_12) +FN:280,(anonymous_13) +FN:380,(anonymous_14) +FN:395,(anonymous_15) +FN:442,(anonymous_16) +FN:446,(anonymous_17) +FN:473,(anonymous_18) +FN:529,(anonymous_19) +FN:538,(anonymous_20) +FN:553,(anonymous_21) +FN:583,(anonymous_22) +FN:594,(anonymous_23) +FN:604,(anonymous_24) +FN:607,(anonymous_25) +FN:648,(anonymous_26) +FN:697,(anonymous_27) +FN:746,(anonymous_28) +FN:767,(anonymous_29) +FN:783,(anonymous_30) +FN:787,(anonymous_31) +FN:801,(anonymous_32) +FN:816,(anonymous_33) +FN:854,(anonymous_34) +FN:858,(anonymous_35) +FN:877,(anonymous_36) +FN:883,(anonymous_37) +FN:889,(anonymous_38) +FN:895,(anonymous_39) +FN:907,(anonymous_40) +FN:927,(anonymous_41) +FN:964,(anonymous_42) +FN:975,(anonymous_43) +FN:983,(anonymous_44) +FN:1012,(anonymous_45) +FNF:42 +FNH:29 +FNDA:35,(anonymous_4) +FNDA:16,(anonymous_5) +FNDA:4,(anonymous_6) +FNDA:3,(anonymous_7) +FNDA:3,(anonymous_8) +FNDA:62,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:4,(anonymous_11) +FNDA:61,(anonymous_12) +FNDA:2,(anonymous_13) +FNDA:2,(anonymous_14) +FNDA:7,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:2,(anonymous_17) +FNDA:3,(anonymous_18) +FNDA:2,(anonymous_19) +FNDA:1,(anonymous_20) +FNDA:2,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:5,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:3,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:1,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:2,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:1,(anonymous_34) +FNDA:4,(anonymous_35) +FNDA:1,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:10,(anonymous_40) +FNDA:9,(anonymous_41) +FNDA:2,(anonymous_42) +FNDA:4,(anonymous_43) +FNDA:4,(anonymous_44) +FNDA:1,(anonymous_45) +DA:1,35 +DA:24,35 +DA:31,35 +DA:33,35 +DA:34,35 +DA:35,35 +DA:36,35 +DA:38,35 +DA:39,35 +DA:40,35 +DA:41,35 +DA:42,35 +DA:43,35 +DA:44,35 +DA:45,35 +DA:49,35 +DA:53,35 +DA:54,35 +DA:55,35 +DA:59,35 +DA:63,35 +DA:67,35 +DA:71,35 +DA:75,35 +DA:79,35 +DA:83,35 +DA:87,35 +DA:90,35 +DA:93,35 +DA:95,35 +DA:97,35 +DA:99,35 +DA:101,35 +DA:103,35 +DA:105,35 +DA:107,35 +DA:108,35 +DA:109,35 +DA:110,35 +DA:111,35 +DA:112,35 +DA:116,16 +DA:117,16 +DA:120,15 +DA:139,15 +DA:140,15 +DA:147,4 +DA:149,4 +DA:152,3 +DA:156,3 +DA:159,3 +DA:161,4 +DA:165,62 +DA:166,62 +DA:167,1 +DA:169,61 +DA:171,60 +DA:184,0 +DA:187,0 +DA:190,0 +DA:192,0 +DA:193,0 +DA:195,0 +DA:197,0 +DA:201,0 +DA:203,0 +DA:212,4 +DA:215,3 +DA:218,3 +DA:220,0 +DA:221,0 +DA:223,0 +DA:225,0 +DA:229,0 +DA:231,0 +DA:235,0 +DA:237,0 +DA:250,61 +DA:253,4 +DA:256,4 +DA:258,2 +DA:259,2 +DA:261,2 +DA:263,1 +DA:267,1 +DA:269,1 +DA:273,1 +DA:275,1 +DA:285,2 +DA:287,2 +DA:291,1 +DA:299,1 +DA:300,0 +DA:301,0 +DA:305,0 +DA:314,1 +DA:352,1 +DA:355,1 +DA:360,0 +DA:363,1 +DA:368,0 +DA:377,1 +DA:381,2 +DA:383,2 +DA:384,2 +DA:385,1 +DA:392,1 +DA:398,7 +DA:399,7 +DA:400,7 +DA:404,0 +DA:413,7 +DA:417,7 +DA:418,7 +DA:420,7 +DA:421,0 +DA:426,7 +DA:439,7 +DA:443,0 +DA:447,2 +DA:449,2 +DA:450,0 +DA:455,2 +DA:456,2 +DA:457,2 +DA:461,1 +DA:470,1 +DA:476,3 +DA:478,3 +DA:479,1 +DA:487,2 +DA:490,2 +DA:497,2 +DA:502,2 +DA:514,2 +DA:526,2 +DA:534,2 +DA:535,2 +DA:539,1 +DA:541,1 +DA:542,0 +DA:550,1 +DA:557,2 +DA:559,2 +DA:563,1 +DA:571,1 +DA:580,1 +DA:588,0 +DA:589,0 +DA:591,0 +DA:598,0 +DA:599,0 +DA:602,0 +DA:604,0 +DA:610,5 +DA:612,5 +DA:618,1 +DA:628,4 +DA:645,4 +DA:653,0 +DA:655,0 +DA:656,0 +DA:658,0 +DA:662,0 +DA:670,0 +DA:671,0 +DA:673,0 +DA:675,0 +DA:676,0 +DA:686,0 +DA:705,3 +DA:706,3 +DA:707,3 +DA:708,0 +DA:712,3 +DA:715,3 +DA:716,3 +DA:717,1 +DA:722,2 +DA:726,2 +DA:750,0 +DA:752,0 +DA:754,0 +DA:756,0 +DA:758,0 +DA:760,0 +DA:772,1 +DA:774,1 +DA:775,0 +DA:780,1 +DA:784,0 +DA:788,2 +DA:789,2 +DA:798,2 +DA:806,0 +DA:807,0 +DA:820,0 +DA:823,0 +DA:826,0 +DA:828,0 +DA:830,0 +DA:831,0 +DA:833,0 +DA:835,0 +DA:839,0 +DA:841,0 +DA:845,0 +DA:847,0 +DA:855,1 +DA:861,4 +DA:862,4 +DA:865,4 +DA:874,4 +DA:880,1 +DA:886,0 +DA:892,0 +DA:896,0 +DA:914,10 +DA:915,10 +DA:918,10 +DA:920,10 +DA:921,1 +DA:927,9 +DA:929,9 +DA:935,9 +DA:944,9 +DA:948,9 +DA:967,2 +DA:976,4 +DA:979,4 +DA:982,4 +DA:984,4 +DA:987,4 +DA:990,4 +DA:991,3 +DA:999,1 +DA:1017,1 +LF:237 +LH:166 +BRDA:116,0,0,16 +BRDA:116,0,1,0 +BRDA:128,1,0,15 +BRDA:128,1,1,15 +BRDA:129,2,0,15 +BRDA:129,2,1,15 +BRDA:130,3,0,15 +BRDA:130,3,1,15 +BRDA:131,4,0,15 +BRDA:131,4,1,15 +BRDA:132,5,0,15 +BRDA:132,5,1,15 +BRDA:133,6,0,15 +BRDA:133,6,1,15 +BRDA:134,7,0,15 +BRDA:134,7,1,0 +BRDA:135,8,0,15 +BRDA:135,8,1,15 +BRDA:136,9,0,15 +BRDA:136,9,1,15 +BRDA:156,10,0,2 +BRDA:156,10,1,1 +BRDA:166,11,0,1 +BRDA:166,11,1,61 +BRDA:184,12,0,0 +BRDA:184,12,1,0 +BRDA:187,13,0,0 +BRDA:187,13,1,0 +BRDA:190,14,0,0 +BRDA:190,14,1,0 +BRDA:195,15,0,0 +BRDA:195,15,1,0 +BRDA:195,16,0,0 +BRDA:195,16,1,0 +BRDA:201,17,0,0 +BRDA:201,17,1,0 +BRDA:212,18,0,1 +BRDA:212,18,1,3 +BRDA:215,19,0,3 +BRDA:215,19,1,2 +BRDA:218,20,0,3 +BRDA:218,20,1,0 +BRDA:223,21,0,0 +BRDA:223,21,1,0 +BRDA:223,22,0,0 +BRDA:223,22,1,0 +BRDA:229,23,0,0 +BRDA:229,23,1,0 +BRDA:235,24,0,0 +BRDA:235,24,1,0 +BRDA:250,25,0,57 +BRDA:250,25,1,4 +BRDA:253,26,0,4 +BRDA:253,26,1,4 +BRDA:256,27,0,2 +BRDA:256,27,1,2 +BRDA:261,28,0,1 +BRDA:261,28,1,1 +BRDA:261,29,0,2 +BRDA:261,29,1,2 +BRDA:267,30,0,0 +BRDA:267,30,1,1 +BRDA:273,31,0,0 +BRDA:273,31,1,1 +BRDA:287,32,0,1 +BRDA:287,32,1,1 +BRDA:288,33,0,2 +BRDA:288,33,1,1 +BRDA:299,34,0,0 +BRDA:299,34,1,1 +BRDA:301,35,0,0 +BRDA:301,35,1,0 +BRDA:302,36,0,0 +BRDA:302,36,1,0 +BRDA:315,37,0,0 +BRDA:315,37,1,1 +BRDA:316,38,0,0 +BRDA:316,38,1,1 +BRDA:319,39,0,0 +BRDA:319,39,1,1 +BRDA:322,40,0,0 +BRDA:322,40,1,1 +BRDA:325,41,0,0 +BRDA:325,41,1,1 +BRDA:326,42,0,0 +BRDA:326,42,1,1 +BRDA:329,43,0,1 +BRDA:329,43,1,0 +BRDA:330,44,0,0 +BRDA:330,44,1,1 +BRDA:331,45,0,0 +BRDA:331,45,1,1 +BRDA:334,46,0,0 +BRDA:334,46,1,1 +BRDA:337,47,0,0 +BRDA:337,47,1,1 +BRDA:340,48,0,0 +BRDA:340,48,1,1 +BRDA:343,49,0,0 +BRDA:343,49,1,1 +BRDA:346,50,0,0 +BRDA:346,50,1,1 +BRDA:347,51,0,0 +BRDA:347,51,1,1 +BRDA:355,52,0,0 +BRDA:355,52,1,1 +BRDA:356,53,0,1 +BRDA:356,53,1,0 +BRDA:356,53,2,0 +BRDA:363,54,0,0 +BRDA:363,54,1,1 +BRDA:364,55,0,1 +BRDA:364,55,1,0 +BRDA:364,55,2,0 +BRDA:384,56,0,1 +BRDA:384,56,1,1 +BRDA:398,57,0,7 +BRDA:398,57,1,0 +BRDA:400,58,0,0 +BRDA:400,58,1,7 +BRDA:401,59,0,7 +BRDA:401,59,1,0 +BRDA:413,60,0,1 +BRDA:413,60,1,6 +BRDA:417,61,0,7 +BRDA:417,61,1,1 +BRDA:418,62,0,7 +BRDA:418,62,1,1 +BRDA:420,63,0,0 +BRDA:420,63,1,7 +BRDA:420,64,0,7 +BRDA:420,64,1,7 +BRDA:427,65,0,7 +BRDA:427,65,1,0 +BRDA:428,66,0,7 +BRDA:428,66,1,6 +BRDA:432,67,0,7 +BRDA:432,67,1,6 +BRDA:432,67,2,5 +BRDA:434,68,0,7 +BRDA:434,68,1,7 +BRDA:434,68,2,6 +BRDA:449,69,0,0 +BRDA:449,69,1,2 +BRDA:455,70,0,2 +BRDA:455,70,1,0 +BRDA:457,71,0,1 +BRDA:457,71,1,1 +BRDA:458,72,0,2 +BRDA:458,72,1,1 +BRDA:478,73,0,1 +BRDA:478,73,1,2 +BRDA:498,74,0,2 +BRDA:498,74,1,0 +BRDA:502,75,0,2 +BRDA:502,75,1,2 +BRDA:505,76,0,2 +BRDA:505,76,1,0 +BRDA:541,77,0,0 +BRDA:541,77,1,1 +BRDA:559,78,0,1 +BRDA:559,78,1,1 +BRDA:560,79,0,2 +BRDA:560,79,1,1 +BRDA:576,80,0,1 +BRDA:576,80,1,0 +BRDA:577,81,0,1 +BRDA:577,81,1,1 +BRDA:612,82,0,1 +BRDA:612,82,1,4 +BRDA:613,83,0,5 +BRDA:613,83,1,4 +BRDA:613,83,2,1 +BRDA:613,83,3,1 +BRDA:658,84,0,0 +BRDA:658,84,1,0 +BRDA:659,85,0,0 +BRDA:659,85,1,0 +BRDA:675,86,0,0 +BRDA:675,86,1,0 +BRDA:701,87,0,3 +BRDA:702,88,0,3 +BRDA:703,89,0,3 +BRDA:707,90,0,0 +BRDA:707,90,1,3 +BRDA:716,91,0,1 +BRDA:716,91,1,2 +BRDA:750,92,0,0 +BRDA:750,92,1,0 +BRDA:754,93,0,0 +BRDA:754,93,1,0 +BRDA:758,94,0,0 +BRDA:758,94,1,0 +BRDA:774,95,0,0 +BRDA:774,95,1,1 +BRDA:820,96,0,0 +BRDA:820,96,1,0 +BRDA:823,97,0,0 +BRDA:823,97,1,0 +BRDA:826,98,0,0 +BRDA:826,98,1,0 +BRDA:833,99,0,0 +BRDA:833,99,1,0 +BRDA:833,100,0,0 +BRDA:833,100,1,0 +BRDA:839,101,0,0 +BRDA:839,101,1,0 +BRDA:845,102,0,0 +BRDA:845,102,1,0 +BRDA:861,103,0,4 +BRDA:861,103,1,0 +BRDA:866,104,0,4 +BRDA:866,104,1,0 +BRDA:871,105,0,4 +BRDA:871,105,1,0 +BRDA:910,106,0,4 +BRDA:911,107,0,4 +BRDA:920,108,0,1 +BRDA:920,108,1,9 +BRDA:939,109,0,9 +BRDA:939,109,1,8 +BRDA:984,110,0,4 +BRDA:984,110,1,3 +BRDA:990,111,0,3 +BRDA:990,111,1,1 +BRDA:990,112,0,4 +BRDA:990,112,1,3 +BRDA:93,113,0,0 +BRDA:93,113,1,35 +BRDA:93,114,0,35 +BRDA:93,114,1,0 +BRDA:93,115,0,0 +BRDA:93,115,1,35 +BRDA:93,116,0,35 +BRDA:93,116,1,0 +BRDA:95,117,0,0 +BRDA:95,117,1,35 +BRDA:95,118,0,35 +BRDA:95,118,1,0 +BRDA:97,119,0,0 +BRDA:97,119,1,35 +BRDA:97,120,0,35 +BRDA:97,120,1,0 +BRDA:99,121,0,0 +BRDA:99,121,1,35 +BRDA:99,122,0,35 +BRDA:99,122,1,0 +BRDA:101,123,0,0 +BRDA:101,123,1,35 +BRDA:101,124,0,35 +BRDA:101,124,1,0 +BRDA:103,125,0,0 +BRDA:103,125,1,35 +BRDA:103,126,0,35 +BRDA:103,126,1,0 +BRDA:105,127,0,0 +BRDA:105,127,1,35 +BRDA:105,128,0,35 +BRDA:105,128,1,0 +BRDA:107,129,0,35 +BRDA:107,129,1,0 +BRDA:107,130,0,35 +BRDA:107,130,1,35 +BRDA:108,131,0,35 +BRDA:108,131,1,0 +BRDA:108,132,0,35 +BRDA:108,132,1,35 +BRDA:109,133,0,35 +BRDA:109,133,1,0 +BRDA:109,134,0,35 +BRDA:109,134,1,35 +BRDA:110,135,0,35 +BRDA:110,135,1,0 +BRDA:110,136,0,35 +BRDA:110,136,1,35 +BRDA:111,137,0,35 +BRDA:111,137,1,0 +BRDA:111,138,0,35 +BRDA:111,138,1,35 +BRF:279 +BRH:165 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/answer.repository.ts +FN:23,(anonymous_2) +FN:25,(anonymous_3) +FN:33,(anonymous_4) +FN:40,(anonymous_5) +FN:45,(anonymous_6) +FN:67,(anonymous_7) +FN:79,(anonymous_8) +FN:83,(anonymous_9) +FN:113,(anonymous_10) +FNF:9 +FNH:9 +FNDA:35,(anonymous_2) +FNDA:24,(anonymous_3) +FNDA:14,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:6,(anonymous_6) +FNDA:5,(anonymous_7) +FNDA:1,(anonymous_8) +FNDA:14,(anonymous_9) +FNDA:14,(anonymous_10) +DA:2,35 +DA:8,35 +DA:9,35 +DA:10,35 +DA:15,35 +DA:16,35 +DA:17,35 +DA:20,35 +DA:21,35 +DA:23,35 +DA:26,24 +DA:30,24 +DA:34,14 +DA:35,14 +DA:36,14 +DA:37,14 +DA:41,2 +DA:42,2 +DA:49,6 +DA:55,6 +DA:57,6 +DA:60,4 +DA:67,4 +DA:68,5 +DA:69,5 +DA:80,1 +DA:91,14 +DA:92,14 +DA:116,14 +DA:117,14 +DA:118,14 +LF:31 +LH:31 +BRDA:42,0,0,1 +BRDA:42,0,1,1 +BRDA:57,1,0,2 +BRDA:57,1,1,4 +BRDA:93,2,0,12 +BRDA:93,2,1,2 +BRDA:94,3,0,0 +BRDA:94,3,1,14 +BRDA:95,4,0,1 +BRDA:95,4,1,13 +BRDA:101,5,0,7 +BRDA:101,5,1,7 +BRDA:106,6,0,0 +BRDA:106,6,1,14 +BRDA:107,7,0,0 +BRDA:107,7,1,14 +BRDA:116,8,0,14 +BRDA:116,8,1,13 +BRDA:117,9,0,14 +BRDA:117,9,1,13 +BRDA:23,10,0,35 +BRDA:23,10,1,0 +BRDA:23,11,0,35 +BRDA:23,11,1,35 +BRF:24 +BRH:20 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score-analytics.repository.ts +FN:23,(anonymous_2) +FN:25,(anonymous_3) +FN:49,(anonymous_4) +FN:56,(anonymous_5) +FN:69,(anonymous_6) +FN:81,(anonymous_7) +FN:85,(anonymous_8) +FN:94,(anonymous_9) +FN:139,(anonymous_10) +FNF:9 +FNH:9 +FNDA:35,(anonymous_2) +FNDA:20,(anonymous_3) +FNDA:8,(anonymous_4) +FNDA:3,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:7,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:3,(anonymous_10) +DA:2,35 +DA:8,35 +DA:9,35 +DA:10,35 +DA:15,35 +DA:16,35 +DA:19,35 +DA:21,35 +DA:23,35 +DA:28,20 +DA:29,20 +DA:46,20 +DA:50,8 +DA:53,8 +DA:59,3 +DA:60,3 +DA:62,3 +DA:66,3 +DA:73,2 +DA:78,2 +DA:82,2 +DA:88,7 +DA:91,7 +DA:105,3 +DA:106,3 +DA:142,3 +DA:143,3 +DA:144,3 +LF:28 +LH:28 +BRDA:53,0,0,5 +BRDA:53,0,1,3 +BRDA:107,1,0,2 +BRDA:107,1,1,1 +BRDA:108,2,0,1 +BRDA:108,2,1,2 +BRDA:109,3,0,0 +BRDA:109,3,1,3 +BRDA:110,4,0,0 +BRDA:110,4,1,3 +BRDA:118,5,0,0 +BRDA:118,5,1,3 +BRDA:124,6,0,0 +BRDA:124,6,1,3 +BRDA:130,7,0,0 +BRDA:130,7,1,3 +BRDA:142,8,0,3 +BRDA:142,8,1,3 +BRDA:143,9,0,3 +BRDA:143,9,1,3 +BRDA:23,10,0,35 +BRDA:23,10,1,0 +BRDA:23,11,0,35 +BRDA:23,11,1,35 +BRF:24 +BRH:18 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/cluster-score.repository.ts +FN:21,(anonymous_2) +FN:23,(anonymous_3) +FN:44,(anonymous_4) +FN:54,(anonymous_5) +FN:66,(anonymous_6) +FN:70,(anonymous_7) +FN:81,(anonymous_8) +FN:84,(anonymous_9) +FN:101,(anonymous_10) +FNF:9 +FNH:9 +FNDA:35,(anonymous_2) +FNDA:12,(anonymous_3) +FNDA:9,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:1,(anonymous_7) +FNDA:1,(anonymous_8) +FNDA:9,(anonymous_9) +FNDA:9,(anonymous_10) +DA:2,35 +DA:7,35 +DA:8,35 +DA:9,35 +DA:15,35 +DA:18,35 +DA:19,35 +DA:21,35 +DA:24,12 +DA:25,12 +DA:41,12 +DA:45,9 +DA:46,9 +DA:47,9 +DA:51,9 +DA:55,2 +DA:62,2 +DA:63,1 +DA:67,1 +DA:73,1 +DA:81,1 +DA:88,9 +DA:89,9 +DA:104,9 +DA:105,9 +DA:106,9 +LF:26 +LH:26 +BRDA:62,0,0,1 +BRDA:62,0,1,1 +BRDA:90,1,0,8 +BRDA:90,1,1,1 +BRDA:91,2,0,1 +BRDA:91,2,1,8 +BRDA:92,3,0,0 +BRDA:92,3,1,9 +BRDA:93,4,0,0 +BRDA:93,4,1,9 +BRDA:94,5,0,0 +BRDA:94,5,1,9 +BRDA:97,6,0,0 +BRDA:97,6,1,9 +BRDA:104,7,0,9 +BRDA:104,7,1,9 +BRDA:105,8,0,9 +BRDA:105,8,1,9 +BRDA:21,9,0,35 +BRDA:21,9,1,0 +BRDA:21,10,0,35 +BRDA:21,10,1,35 +BRF:22 +BRH:17 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/cycle-stage-history.repository.ts +FN:14,(anonymous_2) +FN:16,(anonymous_3) +FN:25,(anonymous_4) +FN:33,(anonymous_5) +FNF:4 +FNH:4 +FNDA:37,(anonymous_2) +FNDA:17,(anonymous_3) +FNDA:4,(anonymous_4) +FNDA:2,(anonymous_5) +DA:1,38 +DA:2,38 +DA:3,38 +DA:8,38 +DA:11,38 +DA:12,37 +DA:14,37 +DA:19,17 +DA:22,17 +DA:26,4 +DA:30,4 +DA:34,2 +DA:37,2 +LF:13 +LH:13 +BRDA:37,0,0,1 +BRDA:37,0,1,1 +BRDA:14,1,0,38 +BRDA:14,1,1,0 +BRDA:14,2,0,38 +BRDA:14,2,1,38 +BRF:6 +BRH:5 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/cycle.repository.ts +FN:21,(anonymous_2) +FN:23,(anonymous_3) +FN:31,(anonymous_4) +FN:36,(anonymous_5) +FN:48,(anonymous_6) +FN:66,(anonymous_7) +FN:70,(anonymous_8) +FN:122,(anonymous_9) +FNF:8 +FNH:8 +FNDA:38,(anonymous_2) +FNDA:308,(anonymous_3) +FNDA:163,(anonymous_4) +FNDA:12,(anonymous_5) +FNDA:7,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:12,(anonymous_8) +FNDA:12,(anonymous_9) +DA:2,38 +DA:8,38 +DA:9,38 +DA:10,38 +DA:15,38 +DA:18,38 +DA:19,38 +DA:21,38 +DA:24,308 +DA:28,308 +DA:32,163 +DA:33,163 +DA:37,12 +DA:38,12 +DA:40,12 +DA:45,12 +DA:52,7 +DA:63,7 +DA:67,2 +DA:84,12 +DA:85,12 +DA:125,12 +DA:126,12 +DA:127,12 +LF:24 +LH:24 +BRDA:33,0,0,152 +BRDA:33,0,1,11 +BRDA:56,1,0,2 +BRDA:56,1,1,5 +BRDA:86,2,0,1 +BRDA:86,2,1,11 +BRDA:89,3,0,0 +BRDA:89,3,1,12 +BRDA:97,4,0,0 +BRDA:97,4,1,12 +BRDA:110,5,0,0 +BRDA:110,5,1,12 +BRDA:111,6,0,0 +BRDA:111,6,1,12 +BRDA:112,7,0,3 +BRDA:112,7,1,9 +BRDA:113,8,0,7 +BRDA:113,8,1,5 +BRDA:114,9,0,0 +BRDA:114,9,1,12 +BRDA:115,10,0,0 +BRDA:115,10,1,12 +BRDA:116,11,0,0 +BRDA:116,11,1,12 +BRDA:117,12,0,0 +BRDA:117,12,1,12 +BRDA:118,13,0,0 +BRDA:118,13,1,12 +BRDA:125,14,0,12 +BRDA:125,14,1,11 +BRDA:126,15,0,12 +BRDA:126,15,1,11 +BRDA:21,16,0,38 +BRDA:21,16,1,0 +BRDA:21,17,0,38 +BRDA:21,17,1,38 +BRF:36 +BRH:26 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/question.repository.ts +FN:20,(anonymous_2) +FN:22,(anonymous_3) +FN:30,(anonymous_4) +FN:37,(anonymous_5) +FN:44,(anonymous_6) +FN:48,(anonymous_7) +FN:82,(anonymous_8) +FNF:7 +FNH:7 +FNDA:44,(anonymous_2) +FNDA:66,(anonymous_3) +FNDA:6,(anonymous_4) +FNDA:10,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:10,(anonymous_7) +FNDA:10,(anonymous_8) +DA:2,44 +DA:7,44 +DA:8,44 +DA:9,44 +DA:14,44 +DA:17,44 +DA:18,44 +DA:20,44 +DA:23,66 +DA:27,66 +DA:31,6 +DA:34,6 +DA:38,10 +DA:39,10 +DA:40,10 +DA:41,10 +DA:45,2 +DA:56,10 +DA:57,10 +DA:85,10 +DA:86,10 +DA:87,10 +LF:22 +LH:22 +BRDA:34,0,0,5 +BRDA:34,0,1,1 +BRDA:58,1,0,3 +BRDA:58,1,1,7 +BRDA:59,2,0,4 +BRDA:59,2,1,6 +BRDA:60,3,0,1 +BRDA:60,3,1,9 +BRDA:63,4,0,1 +BRDA:63,4,1,9 +BRDA:68,5,0,0 +BRDA:68,5,1,10 +BRDA:69,6,0,1 +BRDA:69,6,1,9 +BRDA:70,7,0,1 +BRDA:70,7,1,0 +BRDA:85,8,0,10 +BRDA:85,8,1,9 +BRDA:86,9,0,10 +BRDA:86,9,1,9 +BRDA:20,10,0,44 +BRDA:20,10,1,0 +BRDA:20,11,0,44 +BRDA:20,11,1,44 +BRF:24 +BRH:21 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/respondent.repository.ts +FN:21,(anonymous_2) +FN:23,(anonymous_3) +FN:31,(anonymous_4) +FN:43,(anonymous_5) +FN:58,(anonymous_6) +FN:86,(anonymous_7) +FN:90,(anonymous_8) +FN:158,(anonymous_9) +FNF:8 +FNH:8 +FNDA:37,(anonymous_2) +FNDA:26,(anonymous_3) +FNDA:9,(anonymous_4) +FNDA:18,(anonymous_5) +FNDA:3,(anonymous_6) +FNDA:1,(anonymous_7) +FNDA:18,(anonymous_8) +FNDA:18,(anonymous_9) +DA:2,38 +DA:8,38 +DA:9,38 +DA:10,38 +DA:15,38 +DA:18,38 +DA:19,37 +DA:21,37 +DA:24,26 +DA:28,26 +DA:32,9 +DA:36,9 +DA:37,1 +DA:40,8 +DA:47,18 +DA:48,18 +DA:49,18 +DA:50,18 +DA:55,18 +DA:62,3 +DA:83,3 +DA:87,1 +DA:108,18 +DA:109,18 +DA:161,18 +DA:162,18 +DA:163,18 +LF:27 +LH:27 +BRDA:36,0,0,1 +BRDA:36,0,1,8 +BRDA:66,1,0,0 +BRDA:66,1,1,3 +BRDA:73,2,0,3 +BRDA:73,2,1,0 +BRDA:110,3,0,0 +BRDA:110,3,1,18 +BRDA:111,4,0,1 +BRDA:111,4,1,17 +BRDA:112,5,0,0 +BRDA:112,5,1,18 +BRDA:115,6,0,1 +BRDA:115,6,1,17 +BRDA:120,7,0,1 +BRDA:120,7,1,17 +BRDA:128,8,0,0 +BRDA:128,8,1,18 +BRDA:136,9,0,0 +BRDA:136,9,1,18 +BRDA:139,10,0,0 +BRDA:139,10,1,18 +BRDA:140,11,0,0 +BRDA:140,11,1,18 +BRDA:148,12,0,0 +BRDA:148,12,1,18 +BRDA:149,13,0,0 +BRDA:149,13,1,18 +BRDA:152,14,0,0 +BRDA:152,14,1,18 +BRDA:153,15,0,0 +BRDA:153,15,1,18 +BRDA:154,16,0,0 +BRDA:154,16,1,18 +BRDA:161,17,0,18 +BRDA:161,17,1,17 +BRDA:162,18,0,18 +BRDA:162,18,1,17 +BRDA:21,19,0,38 +BRDA:21,19,1,0 +BRDA:21,20,0,38 +BRDA:21,20,1,38 +BRF:42 +BRH:28 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/review-question-relation.repository.ts +FN:22,(anonymous_2) +FN:24,(anonymous_3) +FN:50,(anonymous_4) +FN:64,(anonymous_5) +FN:75,(anonymous_6) +FN:143,(anonymous_7) +FNF:6 +FNH:6 +FNDA:35,(anonymous_2) +FNDA:15,(anonymous_3) +FNDA:11,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:11,(anonymous_6) +FNDA:11,(anonymous_7) +DA:2,35 +DA:7,35 +DA:8,35 +DA:9,35 +DA:14,35 +DA:15,35 +DA:18,35 +DA:20,35 +DA:22,35 +DA:27,15 +DA:28,15 +DA:47,15 +DA:54,11 +DA:55,11 +DA:56,11 +DA:57,11 +DA:61,11 +DA:65,2 +DA:88,11 +DA:89,11 +DA:146,11 +DA:147,11 +DA:148,11 +LF:23 +LH:23 +BRDA:90,0,0,0 +BRDA:90,0,1,11 +BRDA:91,1,0,0 +BRDA:91,1,1,11 +BRDA:92,2,0,1 +BRDA:92,2,1,10 +BRDA:100,3,0,0 +BRDA:100,3,1,11 +BRDA:105,4,0,0 +BRDA:105,4,1,11 +BRDA:106,5,0,0 +BRDA:106,5,1,11 +BRDA:114,6,0,0 +BRDA:114,6,1,11 +BRDA:122,7,0,0 +BRDA:122,7,1,11 +BRDA:130,8,0,0 +BRDA:130,8,1,11 +BRDA:131,9,0,0 +BRDA:131,9,1,0 +BRDA:146,10,0,11 +BRDA:146,10,1,11 +BRDA:147,11,0,11 +BRDA:147,11,1,11 +BRDA:22,12,0,35 +BRDA:22,12,1,0 +BRDA:22,13,0,35 +BRDA:22,13,1,35 +BRF:28 +BRH:17 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/review-stage-history.repository.ts +FN:15,(anonymous_2) +FN:17,(anonymous_3) +FN:26,(anonymous_4) +FN:36,(anonymous_5) +FNF:4 +FNH:4 +FNDA:35,(anonymous_2) +FNDA:13,(anonymous_3) +FNDA:4,(anonymous_4) +FNDA:2,(anonymous_5) +DA:1,35 +DA:2,35 +DA:3,35 +DA:8,35 +DA:11,35 +DA:13,35 +DA:15,35 +DA:20,13 +DA:23,13 +DA:29,4 +DA:33,4 +DA:37,2 +DA:40,2 +LF:13 +LH:13 +BRDA:40,0,0,1 +BRDA:40,0,1,1 +BRDA:15,1,0,35 +BRDA:15,1,1,0 +BRDA:15,2,0,35 +BRDA:15,2,1,35 +BRF:6 +BRH:5 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/review.repository.ts +FN:21,(anonymous_2) +FN:23,(anonymous_3) +FN:31,(anonymous_4) +FN:36,(anonymous_5) +FN:43,(anonymous_6) +FN:50,(anonymous_7) +FN:68,(anonymous_8) +FN:85,(anonymous_9) +FN:158,(anonymous_10) +FNF:9 +FNH:9 +FNDA:38,(anonymous_2) +FNDA:222,(anonymous_3) +FNDA:103,(anonymous_4) +FNDA:10,(anonymous_5) +FNDA:23,(anonymous_6) +FNDA:5,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:10,(anonymous_9) +FNDA:10,(anonymous_10) +DA:2,38 +DA:8,38 +DA:9,38 +DA:10,38 +DA:15,38 +DA:18,38 +DA:19,38 +DA:21,38 +DA:24,222 +DA:28,222 +DA:32,103 +DA:33,103 +DA:37,10 +DA:38,10 +DA:39,10 +DA:40,10 +DA:44,23 +DA:47,22 +DA:54,5 +DA:65,5 +DA:72,2 +DA:103,10 +DA:104,10 +DA:161,10 +DA:162,10 +DA:163,10 +LF:26 +LH:26 +BRDA:33,0,0,98 +BRDA:33,0,1,5 +BRDA:58,1,0,1 +BRDA:58,1,1,4 +BRDA:105,2,0,0 +BRDA:105,2,1,10 +BRDA:106,3,0,0 +BRDA:106,3,1,10 +BRDA:114,4,0,0 +BRDA:114,4,1,10 +BRDA:115,5,0,0 +BRDA:115,5,1,10 +BRDA:123,6,0,1 +BRDA:123,6,1,9 +BRDA:124,7,0,0 +BRDA:124,7,1,10 +BRDA:127,8,0,0 +BRDA:127,8,1,10 +BRDA:130,9,0,0 +BRDA:130,9,1,10 +BRDA:131,10,0,0 +BRDA:131,10,1,10 +BRDA:134,11,0,0 +BRDA:134,11,1,10 +BRDA:135,12,0,0 +BRDA:135,12,1,10 +BRDA:143,13,0,0 +BRDA:143,13,1,10 +BRDA:144,14,0,0 +BRDA:144,14,1,10 +BRDA:152,15,0,4 +BRDA:152,15,1,6 +BRDA:153,16,0,3 +BRDA:153,16,1,7 +BRDA:154,17,0,0 +BRDA:154,17,1,10 +BRDA:161,18,0,10 +BRDA:161,18,1,9 +BRDA:162,19,0,10 +BRDA:162,19,1,9 +BRDA:21,20,0,38 +BRDA:21,20,1,0 +BRDA:21,21,0,38 +BRDA:21,21,1,38 +BRF:44 +BRH:30 +end_of_record +TN: +SF:src/contexts/feedback360/infrastructure/prisma-repositories/reviewer.repository.ts +FN:20,(anonymous_2) +FN:22,(anonymous_3) +FN:29,(anonymous_4) +FN:40,(anonymous_5) +FN:44,(anonymous_6) +FN:76,(anonymous_7) +FNF:6 +FNH:6 +FNDA:37,(anonymous_2) +FNDA:9,(anonymous_3) +FNDA:11,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:11,(anonymous_6) +FNDA:11,(anonymous_7) +DA:2,38 +DA:7,38 +DA:8,38 +DA:9,38 +DA:14,38 +DA:17,38 +DA:18,37 +DA:20,37 +DA:23,9 +DA:26,9 +DA:33,11 +DA:34,11 +DA:35,11 +DA:36,11 +DA:37,11 +DA:41,2 +DA:53,11 +DA:54,11 +DA:79,11 +DA:80,11 +DA:81,11 +LF:21 +LH:21 +BRDA:55,0,0,0 +BRDA:55,0,1,11 +BRDA:56,1,0,4 +BRDA:56,1,1,7 +BRDA:57,2,0,1 +BRDA:57,2,1,10 +BRDA:60,3,0,0 +BRDA:60,3,1,11 +BRDA:61,4,0,0 +BRDA:61,4,1,11 +BRDA:69,5,0,0 +BRDA:69,5,1,11 +BRDA:70,6,0,0 +BRDA:70,6,1,11 +BRDA:79,7,0,11 +BRDA:79,7,1,10 +BRDA:80,8,0,11 +BRDA:80,8,1,10 +BRDA:20,9,0,38 +BRDA:20,9,1,0 +BRDA:20,10,0,38 +BRDA:20,10,1,38 +BRF:22 +BRH:16 +end_of_record +TN: +SF:src/contexts/identity/application/services/identity-role.service.ts +FN:9,(anonymous_4) +FN:14,(anonymous_5) +FN:18,(anonymous_6) +FNF:3 +FNH:3 +FNDA:57,(anonymous_4) +FNDA:3,(anonymous_5) +FNDA:3,(anonymous_6) +DA:2,57 +DA:5,57 +DA:8,57 +DA:11,57 +DA:15,3 +DA:19,3 +LF:6 +LH:6 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/identity/application/services/identity-user.service.ts +FN:26,(anonymous_4) +FN:34,(anonymous_5) +FN:77,(anonymous_6) +FN:81,(anonymous_7) +FN:90,(anonymous_8) +FN:97,(anonymous_9) +FN:120,(anonymous_10) +FN:125,(anonymous_11) +FN:134,(anonymous_12) +FN:135,(anonymous_13) +FN:144,(anonymous_14) +FN:189,(anonymous_15) +FNF:12 +FNH:10 +FNDA:57,(anonymous_4) +FNDA:671,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:27,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:3,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:3,(anonymous_14) +FNDA:673,(anonymous_15) +DA:1,57 +DA:8,57 +DA:14,57 +DA:15,57 +DA:16,57 +DA:18,57 +DA:20,57 +DA:25,57 +DA:28,57 +DA:30,57 +DA:31,57 +DA:35,671 +DA:55,671 +DA:59,671 +DA:60,1 +DA:65,670 +DA:67,670 +DA:68,670 +DA:74,670 +DA:78,2 +DA:85,27 +DA:86,27 +DA:87,21 +DA:94,2 +DA:98,3 +DA:100,2 +DA:104,2 +DA:112,2 +DA:117,2 +DA:121,2 +DA:122,1 +DA:129,3 +DA:131,3 +DA:132,3 +DA:133,3 +DA:134,0 +DA:135,0 +DA:136,0 +DA:141,3 +DA:151,3 +DA:155,3 +DA:156,1 +DA:159,2 +DA:177,2 +DA:179,2 +DA:180,2 +DA:186,2 +DA:194,673 +DA:197,673 +LF:49 +LH:46 +BRDA:40,0,0,671 +BRDA:40,0,1,670 +BRDA:48,1,0,671 +BRDA:48,1,1,670 +BRDA:49,2,0,671 +BRDA:49,2,1,671 +BRDA:50,3,0,671 +BRDA:50,3,1,671 +BRDA:51,4,0,671 +BRDA:51,4,1,671 +BRDA:52,5,0,671 +BRDA:52,5,1,665 +BRDA:59,6,0,1 +BRDA:59,6,1,670 +BRDA:67,7,0,670 +BRDA:67,7,1,0 +BRDA:86,8,0,6 +BRDA:86,8,1,21 +BRDA:100,9,0,2 +BRDA:100,9,1,1 +BRDA:100,9,2,1 +BRDA:104,10,0,1 +BRDA:104,10,1,1 +BRDA:106,11,0,1 +BRDA:106,11,1,1 +BRDA:107,12,0,1 +BRDA:107,12,1,0 +BRDA:108,13,0,1 +BRDA:108,13,1,1 +BRDA:114,14,0,1 +BRDA:114,14,1,1 +BRDA:133,15,0,0 +BRDA:133,15,1,3 +BRDA:155,16,0,1 +BRDA:155,16,1,2 +BRDA:179,17,0,2 +BRDA:179,17,1,0 +BRDA:194,18,0,673 +BRDA:194,18,1,673 +BRDA:31,19,0,57 +BRDA:31,19,1,0 +BRDA:31,20,0,57 +BRDA:31,20,1,57 +BRF:43 +BRH:38 +end_of_record +TN: +SF:src/contexts/identity/infrastructure/prisma-repositories/role.repository.ts +FN:9,(anonymous_2) +FN:11,(anonymous_3) +FN:18,(anonymous_4) +FNF:3 +FNH:3 +FNDA:57,(anonymous_2) +FNDA:6,(anonymous_3) +FNDA:9,(anonymous_4) +DA:2,57 +DA:3,57 +DA:5,57 +DA:8,57 +DA:9,57 +DA:12,6 +DA:15,6 +DA:19,9 +DA:20,6 +DA:23,6 +LF:10 +LH:10 +BRDA:19,0,0,3 +BRDA:19,0,1,6 +BRDA:9,1,0,57 +BRDA:9,1,1,0 +BRDA:9,2,0,57 +BRDA:9,2,1,57 +BRF:6 +BRH:5 +end_of_record +TN: +SF:src/contexts/identity/infrastructure/prisma-repositories/user.repository.ts +FN:17,(anonymous_2) +FN:19,(anonymous_3) +FN:28,(anonymous_4) +FN:40,(anonymous_5) +FN:52,(anonymous_6) +FN:62,(anonymous_7) +FN:65,(anonymous_8) +FN:83,(anonymous_9) +FN:87,(anonymous_10) +FN:91,(anonymous_11) +FN:99,(anonymous_12) +FN:100,(anonymous_13) +FN:104,(anonymous_14) +FN:117,(anonymous_15) +FN:183,(anonymous_16) +FN:191,(anonymous_17) +FNF:16 +FNH:16 +FNDA:57,(anonymous_2) +FNDA:742,(anonymous_3) +FNDA:56,(anonymous_4) +FNDA:678,(anonymous_5) +FNDA:8,(anonymous_6) +FNDA:13,(anonymous_7) +FNDA:4,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:8,(anonymous_10) +FNDA:8,(anonymous_11) +FNDA:6,(anonymous_12) +FNDA:11,(anonymous_13) +FNDA:5,(anonymous_14) +FNDA:8,(anonymous_15) +FNDA:8,(anonymous_16) +FNDA:1488,(anonymous_17) +DA:2,57 +DA:9,57 +DA:10,57 +DA:13,57 +DA:16,57 +DA:17,57 +DA:20,742 +DA:25,742 +DA:32,56 +DA:37,56 +DA:44,678 +DA:49,678 +DA:53,8 +DA:54,8 +DA:56,8 +DA:62,13 +DA:69,4 +DA:80,4 +DA:84,2 +DA:91,8 +DA:92,8 +DA:96,8 +DA:98,8 +DA:99,8 +DA:100,11 +DA:102,8 +DA:103,3 +DA:104,5 +DA:110,8 +DA:111,8 +DA:112,0 +DA:114,8 +DA:130,8 +DA:132,8 +DA:186,8 +DA:187,8 +DA:188,8 +DA:192,1488 +DA:193,1439 +LF:39 +LH:38 +BRDA:37,0,0,48 +BRDA:37,0,1,8 +BRDA:49,1,0,4 +BRDA:49,1,1,674 +BRDA:73,2,0,4 +BRDA:73,2,1,4 +BRDA:74,3,0,4 +BRDA:74,3,1,4 +BRDA:75,4,0,4 +BRDA:75,4,1,4 +BRDA:92,5,0,6 +BRDA:92,5,1,2 +BRDA:102,6,0,3 +BRDA:102,6,1,5 +BRDA:111,7,0,0 +BRDA:111,7,1,8 +BRDA:133,8,0,0 +BRDA:133,8,1,8 +BRDA:136,9,0,0 +BRDA:136,9,1,8 +BRDA:139,10,0,1 +BRDA:139,10,1,7 +BRDA:142,11,0,0 +BRDA:142,11,1,8 +BRDA:145,12,0,3 +BRDA:145,12,1,5 +BRDA:170,13,0,0 +BRDA:170,13,1,8 +BRDA:173,14,0,2 +BRDA:173,14,1,6 +BRDA:174,15,0,0 +BRDA:174,15,1,8 +BRDA:175,16,0,0 +BRDA:175,16,1,8 +BRDA:176,17,0,0 +BRDA:176,17,1,8 +BRDA:177,18,0,0 +BRDA:177,18,1,8 +BRDA:186,19,0,8 +BRDA:186,19,1,7 +BRDA:187,20,0,8 +BRDA:187,20,1,7 +BRDA:192,21,0,49 +BRDA:192,21,1,1439 +BRDA:17,22,0,57 +BRDA:17,22,1,0 +BRDA:17,23,0,57 +BRDA:17,23,1,57 +BRF:48 +BRH:38 +end_of_record +TN: +SF:src/contexts/library/application/services/cluster.service.ts +FN:21,(anonymous_4) +FN:27,(anonymous_5) +FN:42,(anonymous_6) +FN:46,(anonymous_7) +FN:52,(anonymous_8) +FN:87,(anonymous_9) +FN:92,(anonymous_10) +FNF:7 +FNH:7 +FNDA:44,(anonymous_4) +FNDA:28,(anonymous_5) +FNDA:8,(anonymous_6) +FNDA:7,(anonymous_7) +FNDA:3,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:29,(anonymous_10) +DA:6,44 +DA:12,44 +DA:13,44 +DA:17,44 +DA:20,44 +DA:23,44 +DA:24,44 +DA:28,28 +DA:29,27 +DA:31,26 +DA:39,26 +DA:43,8 +DA:47,7 +DA:48,7 +DA:49,5 +DA:56,3 +DA:58,3 +DA:59,1 +DA:62,2 +DA:63,1 +DA:64,1 +DA:65,1 +DA:68,1 +DA:84,1 +DA:88,2 +DA:89,1 +DA:93,29 +DA:94,2 +LF:28 +LH:28 +BRDA:48,0,0,2 +BRDA:48,0,1,5 +BRDA:58,1,0,1 +BRDA:58,1,1,2 +BRDA:58,2,0,3 +BRDA:58,2,1,1 +BRDA:62,3,0,1 +BRDA:62,3,1,1 +BRDA:62,4,0,2 +BRDA:62,4,1,1 +BRDA:63,5,0,1 +BRDA:63,5,1,0 +BRDA:64,6,0,1 +BRDA:64,6,1,0 +BRDA:69,7,0,0 +BRDA:69,7,1,1 +BRDA:72,8,0,0 +BRDA:72,8,1,1 +BRDA:75,9,0,0 +BRDA:75,9,1,1 +BRDA:78,10,0,0 +BRDA:78,10,1,1 +BRDA:79,11,0,1 +BRDA:79,11,1,0 +BRDA:93,12,0,2 +BRDA:93,12,1,27 +BRDA:23,13,0,0 +BRDA:23,13,1,44 +BRDA:23,14,0,44 +BRDA:23,14,1,0 +BRDA:23,15,0,44 +BRDA:23,15,1,0 +BRDA:23,16,0,44 +BRDA:23,16,1,44 +BRF:34 +BRH:24 +end_of_record +TN: +SF:src/contexts/library/application/services/competence.service.ts +FN:33,(anonymous_4) +FN:45,(anonymous_5) +FN:54,(anonymous_6) +FN:58,(anonymous_7) +FN:64,(anonymous_8) +FN:79,(anonymous_9) +FN:104,(anonymous_10) +FN:115,(anonymous_11) +FN:123,(anonymous_12) +FN:129,(anonymous_13) +FN:132,(anonymous_14) +FN:146,(anonymous_15) +FN:157,(anonymous_16) +FN:163,(anonymous_17) +FN:166,(anonymous_18) +FNF:15 +FNH:15 +FNDA:44,(anonymous_4) +FNDA:102,(anonymous_5) +FNDA:3,(anonymous_6) +FNDA:114,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:4,(anonymous_9) +FNDA:5,(anonymous_10) +FNDA:2,(anonymous_11) +FNDA:8,(anonymous_12) +FNDA:5,(anonymous_13) +FNDA:3,(anonymous_14) +FNDA:1,(anonymous_15) +FNDA:6,(anonymous_16) +FNDA:3,(anonymous_17) +FNDA:3,(anonymous_18) +DA:6,44 +DA:12,44 +DA:13,44 +DA:14,44 +DA:18,44 +DA:22,44 +DA:26,44 +DA:32,44 +DA:35,44 +DA:37,44 +DA:39,44 +DA:41,44 +DA:42,44 +DA:46,102 +DA:51,102 +DA:55,3 +DA:59,114 +DA:60,114 +DA:61,106 +DA:68,2 +DA:69,1 +DA:76,1 +DA:80,4 +DA:81,3 +DA:82,3 +DA:83,3 +DA:87,2 +DA:97,1 +DA:98,1 +DA:101,1 +DA:108,5 +DA:109,5 +DA:111,4 +DA:112,4 +DA:119,2 +DA:120,2 +DA:124,8 +DA:126,8 +DA:129,8 +DA:136,3 +DA:137,3 +DA:139,2 +DA:143,2 +DA:150,1 +DA:151,1 +DA:158,6 +DA:160,6 +DA:163,6 +DA:169,3 +DA:170,3 +LF:50 +LH:50 +BRDA:47,0,0,102 +BRDA:47,0,1,92 +BRDA:49,1,0,102 +BRDA:49,1,1,91 +BRDA:60,2,0,8 +BRDA:60,2,1,106 +BRDA:70,3,0,0 +BRDA:70,3,1,1 +BRDA:71,4,0,1 +BRDA:71,4,1,0 +BRDA:72,5,0,0 +BRDA:72,5,1,1 +BRDA:83,6,0,2 +BRDA:83,6,1,1 +BRDA:84,7,0,3 +BRDA:84,7,1,2 +BRDA:170,8,0,1 +BRDA:170,8,1,2 +BRDA:35,9,0,0 +BRDA:35,9,1,44 +BRDA:35,10,0,44 +BRDA:35,10,1,0 +BRDA:35,11,0,0 +BRDA:35,11,1,44 +BRDA:35,12,0,44 +BRDA:35,12,1,0 +BRDA:37,13,0,0 +BRDA:37,13,1,44 +BRDA:37,14,0,44 +BRDA:37,14,1,0 +BRDA:39,15,0,0 +BRDA:39,15,1,44 +BRDA:39,16,0,44 +BRDA:39,16,1,0 +BRDA:41,17,0,44 +BRDA:41,17,1,0 +BRDA:41,18,0,44 +BRDA:41,18,1,44 +BRF:38 +BRH:26 +end_of_record +TN: +SF:src/contexts/library/application/services/question-template.service.ts +FN:35,(anonymous_4) +FN:48,(anonymous_5) +FN:78,(anonymous_6) +FN:84,(anonymous_7) +FN:91,(anonymous_8) +FN:132,(anonymous_9) +FN:155,(anonymous_10) +FN:169,(anonymous_11) +FN:180,(anonymous_12) +FN:186,(anonymous_13) +FN:189,(anonymous_14) +FN:203,(anonymous_15) +FN:214,(anonymous_16) +FN:220,(anonymous_17) +FN:223,(anonymous_18) +FN:224,(anonymous_19) +FNF:16 +FNH:16 +FNDA:44,(anonymous_4) +FNDA:47,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:68,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:3,(anonymous_10) +FNDA:1,(anonymous_11) +FNDA:3,(anonymous_12) +FNDA:2,(anonymous_13) +FNDA:3,(anonymous_14) +FNDA:1,(anonymous_15) +FNDA:3,(anonymous_16) +FNDA:2,(anonymous_17) +FNDA:1,(anonymous_18) +FNDA:2,(anonymous_19) +DA:1,44 +DA:7,44 +DA:13,44 +DA:17,44 +DA:18,44 +DA:19,44 +DA:23,44 +DA:27,44 +DA:31,44 +DA:34,44 +DA:37,44 +DA:39,44 +DA:41,44 +DA:42,44 +DA:43,44 +DA:45,44 +DA:51,47 +DA:53,46 +DA:62,46 +DA:64,45 +DA:67,45 +DA:68,1 +DA:69,1 +DA:75,45 +DA:81,1 +DA:85,68 +DA:86,68 +DA:87,1 +DA:88,67 +DA:95,2 +DA:97,2 +DA:98,1 +DA:101,1 +DA:118,1 +DA:120,1 +DA:121,0 +DA:122,0 +DA:123,0 +DA:129,1 +DA:133,2 +DA:134,2 +DA:137,2 +DA:138,1 +DA:146,1 +DA:149,1 +DA:152,1 +DA:159,3 +DA:160,3 +DA:162,2 +DA:166,2 +DA:173,1 +DA:174,1 +DA:181,3 +DA:183,3 +DA:186,3 +DA:193,3 +DA:194,3 +DA:196,2 +DA:200,2 +DA:207,1 +DA:208,1 +DA:215,3 +DA:217,3 +DA:220,3 +DA:224,2 +LF:65 +LH:62 +BRDA:57,0,0,46 +BRDA:57,0,1,46 +BRDA:58,1,0,46 +BRDA:58,1,1,46 +BRDA:59,2,0,46 +BRDA:59,2,1,44 +BRDA:64,3,0,1 +BRDA:64,3,1,44 +BRDA:67,4,0,1 +BRDA:67,4,1,44 +BRDA:86,5,0,1 +BRDA:86,5,1,67 +BRDA:97,6,0,1 +BRDA:97,6,1,1 +BRDA:97,7,0,2 +BRDA:97,7,1,1 +BRDA:102,8,0,0 +BRDA:102,8,1,1 +BRDA:105,9,0,1 +BRDA:105,9,1,0 +BRDA:106,10,0,0 +BRDA:106,10,1,1 +BRDA:109,11,0,0 +BRDA:109,11,1,1 +BRDA:112,12,0,0 +BRDA:112,12,1,1 +BRDA:113,13,0,0 +BRDA:113,13,1,1 +BRDA:120,14,0,0 +BRDA:120,14,1,1 +BRDA:137,15,0,1 +BRDA:137,15,1,1 +BRDA:37,16,0,0 +BRDA:37,16,1,44 +BRDA:37,17,0,44 +BRDA:37,17,1,0 +BRDA:37,18,0,0 +BRDA:37,18,1,44 +BRDA:37,19,0,44 +BRDA:37,19,1,0 +BRDA:39,20,0,0 +BRDA:39,20,1,44 +BRDA:39,21,0,44 +BRDA:39,21,1,0 +BRDA:41,22,0,44 +BRDA:41,22,1,0 +BRDA:41,23,0,44 +BRDA:41,23,1,44 +BRDA:42,24,0,44 +BRDA:42,24,1,0 +BRDA:42,25,0,44 +BRDA:42,25,1,44 +BRDA:43,26,0,0 +BRDA:43,26,1,44 +BRDA:43,27,0,44 +BRDA:43,27,1,0 +BRF:56 +BRH:39 +end_of_record +TN: +SF:src/contexts/library/infrastructure/prisma-repositories/cluster.repository.ts +FN:16,(anonymous_2) +FN:18,(anonymous_3) +FN:26,(anonymous_4) +FN:31,(anonymous_5) +FN:39,(anonymous_6) +FN:51,(anonymous_7) +FN:55,(anonymous_8) +FN:95,(anonymous_9) +FNF:8 +FNH:8 +FNDA:44,(anonymous_2) +FNDA:56,(anonymous_3) +FNDA:9,(anonymous_4) +FNDA:12,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:12,(anonymous_8) +FNDA:12,(anonymous_9) +DA:2,44 +DA:8,44 +DA:9,44 +DA:12,44 +DA:15,44 +DA:16,44 +DA:19,56 +DA:23,56 +DA:27,9 +DA:28,9 +DA:32,12 +DA:33,12 +DA:35,12 +DA:36,12 +DA:43,2 +DA:48,2 +DA:52,2 +DA:63,12 +DA:64,12 +DA:98,12 +DA:99,12 +DA:100,12 +LF:22 +LH:22 +BRDA:28,0,0,6 +BRDA:28,0,1,3 +BRDA:65,1,0,2 +BRDA:65,1,1,10 +BRDA:66,2,0,0 +BRDA:66,2,1,12 +BRDA:67,3,0,0 +BRDA:67,3,1,12 +BRDA:68,4,0,1 +BRDA:68,4,1,11 +BRDA:71,5,0,0 +BRDA:71,5,1,12 +BRDA:79,6,0,0 +BRDA:79,6,1,12 +BRDA:98,7,0,12 +BRDA:98,7,1,11 +BRDA:99,8,0,12 +BRDA:99,8,1,11 +BRDA:16,9,0,44 +BRDA:16,9,1,0 +BRDA:16,10,0,44 +BRDA:16,10,1,44 +BRF:22 +BRH:17 +end_of_record +TN: +SF:src/contexts/library/infrastructure/prisma-repositories/competence-question-template-relation.repository.ts +FN:10,(anonymous_2) +FN:12,(anonymous_3) +FN:47,(anonymous_4) +FN:56,(anonymous_5) +FN:67,(anonymous_6) +FN:78,(anonymous_7) +FN:84,(anonymous_8) +FN:98,(anonymous_9) +FN:110,(anonymous_10) +FN:118,(anonymous_11) +FNF:10 +FNH:7 +FNDA:44,(anonymous_2) +FNDA:14,(anonymous_3) +FNDA:3,(anonymous_4) +FNDA:8,(anonymous_5) +FNDA:5,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:2,(anonymous_11) +DA:1,44 +DA:2,44 +DA:3,44 +DA:6,44 +DA:9,44 +DA:10,44 +DA:16,14 +DA:18,14 +DA:21,13 +DA:23,1 +DA:28,1 +DA:38,1 +DA:39,1 +DA:43,0 +DA:51,3 +DA:60,8 +DA:64,8 +DA:71,5 +DA:75,5 +DA:82,0 +DA:84,0 +DA:86,0 +DA:92,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:107,0 +DA:113,2 +DA:119,2 +LF:29 +LH:20 +BRDA:23,0,0,1 +BRDA:23,0,1,0 +BRDA:24,1,0,1 +BRDA:24,1,1,1 +BRDA:38,2,0,1 +BRDA:38,2,1,0 +BRDA:86,3,0,0 +BRDA:86,3,1,0 +BRDA:96,4,0,0 +BRDA:96,4,1,0 +BRDA:10,5,0,44 +BRDA:10,5,1,0 +BRDA:10,6,0,44 +BRDA:10,6,1,44 +BRF:14 +BRH:7 +end_of_record +TN: +SF:src/contexts/library/infrastructure/prisma-repositories/competence.repository.ts +FN:16,(anonymous_2) +FN:18,(anonymous_3) +FN:26,(anonymous_4) +FN:33,(anonymous_5) +FN:41,(anonymous_6) +FN:53,(anonymous_7) +FN:57,(anonymous_8) +FN:91,(anonymous_9) +FNF:8 +FNH:8 +FNDA:44,(anonymous_2) +FNDA:217,(anonymous_3) +FNDA:118,(anonymous_4) +FNDA:6,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:6,(anonymous_8) +FNDA:6,(anonymous_9) +DA:2,44 +DA:8,44 +DA:9,44 +DA:12,44 +DA:15,44 +DA:16,44 +DA:19,217 +DA:23,217 +DA:27,118 +DA:30,118 +DA:34,6 +DA:35,6 +DA:37,6 +DA:38,6 +DA:45,2 +DA:50,2 +DA:54,2 +DA:60,6 +DA:61,6 +DA:94,6 +DA:95,6 +DA:96,6 +LF:22 +LH:22 +BRDA:30,0,0,109 +BRDA:30,0,1,9 +BRDA:62,1,0,1 +BRDA:62,1,1,5 +BRDA:63,2,0,1 +BRDA:63,2,1,5 +BRDA:66,3,0,0 +BRDA:66,3,1,6 +BRDA:74,4,0,1 +BRDA:74,4,1,5 +BRDA:94,5,0,6 +BRDA:94,5,1,5 +BRDA:95,6,0,6 +BRDA:95,6,1,5 +BRDA:16,7,0,44 +BRDA:16,7,1,0 +BRDA:16,8,0,44 +BRDA:16,8,1,44 +BRF:18 +BRH:16 +end_of_record +TN: +SF:src/contexts/library/infrastructure/prisma-repositories/position-competence-relation.repository.ts +FN:10,(anonymous_2) +FN:12,(anonymous_3) +FN:43,(anonymous_4) +FN:49,(anonymous_5) +FN:61,(anonymous_6) +FN:73,(anonymous_7) +FN:79,(anonymous_8) +FN:90,(anonymous_9) +FN:102,(anonymous_10) +FNF:9 +FNH:9 +FNDA:50,(anonymous_2) +FNDA:17,(anonymous_3) +FNDA:4,(anonymous_4) +FNDA:12,(anonymous_5) +FNDA:5,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:2,(anonymous_10) +DA:1,50 +DA:2,50 +DA:3,50 +DA:6,50 +DA:9,50 +DA:10,50 +DA:16,17 +DA:18,17 +DA:21,15 +DA:23,2 +DA:28,2 +DA:36,2 +DA:37,2 +DA:39,0 +DA:44,4 +DA:52,12 +DA:58,12 +DA:64,5 +DA:70,5 +DA:77,2 +DA:79,2 +DA:81,2 +DA:84,2 +DA:88,2 +DA:89,1 +DA:90,2 +DA:99,2 +DA:103,2 +LF:28 +LH:27 +BRDA:23,0,0,2 +BRDA:23,0,1,0 +BRDA:24,1,0,2 +BRDA:24,1,1,2 +BRDA:36,2,0,2 +BRDA:36,2,1,0 +BRDA:81,3,0,1 +BRDA:81,3,1,1 +BRDA:88,4,0,1 +BRDA:88,4,1,1 +BRDA:10,5,0,50 +BRDA:10,5,1,0 +BRDA:10,6,0,50 +BRDA:10,6,1,50 +BRF:14 +BRH:11 +end_of_record +TN: +SF:src/contexts/library/infrastructure/prisma-repositories/position-question-template-relation.repository.ts +FN:10,(anonymous_2) +FN:12,(anonymous_3) +FN:47,(anonymous_4) +FN:53,(anonymous_5) +FN:65,(anonymous_6) +FN:71,(anonymous_7) +FN:85,(anonymous_8) +FN:97,(anonymous_9) +FNF:8 +FNH:8 +FNDA:44,(anonymous_2) +FNDA:13,(anonymous_3) +FNDA:2,(anonymous_4) +FNDA:9,(anonymous_5) +FNDA:4,(anonymous_6) +FNDA:4,(anonymous_7) +FNDA:5,(anonymous_8) +FNDA:2,(anonymous_9) +DA:1,44 +DA:2,44 +DA:3,44 +DA:6,44 +DA:9,44 +DA:10,44 +DA:16,13 +DA:18,13 +DA:21,12 +DA:23,1 +DA:28,1 +DA:38,1 +DA:39,1 +DA:43,0 +DA:48,2 +DA:57,9 +DA:62,9 +DA:69,4 +DA:71,4 +DA:73,4 +DA:79,4 +DA:83,4 +DA:84,3 +DA:85,5 +DA:94,4 +DA:100,2 +LF:26 +LH:25 +BRDA:23,0,0,1 +BRDA:23,0,1,0 +BRDA:24,1,0,1 +BRDA:24,1,1,1 +BRDA:38,2,0,1 +BRDA:38,2,1,0 +BRDA:73,3,0,3 +BRDA:73,3,1,1 +BRDA:83,4,0,3 +BRDA:83,4,1,1 +BRDA:10,5,0,44 +BRDA:10,5,1,0 +BRDA:10,6,0,44 +BRDA:10,6,1,44 +BRF:14 +BRH:11 +end_of_record +TN: +SF:src/contexts/library/infrastructure/prisma-repositories/question-template.repository.ts +FN:16,(anonymous_2) +FN:18,(anonymous_3) +FN:23,(anonymous_4) +FN:35,(anonymous_5) +FN:44,(anonymous_6) +FN:59,(anonymous_7) +FN:74,(anonymous_8) +FN:78,(anonymous_9) +FN:117,(anonymous_10) +FNF:9 +FNH:9 +FNDA:44,(anonymous_2) +FNDA:75,(anonymous_3) +FNDA:6,(anonymous_4) +FNDA:73,(anonymous_5) +FNDA:5,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:5,(anonymous_9) +FNDA:5,(anonymous_10) +DA:2,44 +DA:8,44 +DA:9,44 +DA:12,44 +DA:15,44 +DA:16,44 +DA:21,75 +DA:22,75 +DA:23,6 +DA:27,75 +DA:32,74 +DA:36,73 +DA:41,73 +DA:47,5 +DA:48,5 +DA:50,5 +DA:56,5 +DA:63,2 +DA:71,2 +DA:75,2 +DA:88,5 +DA:90,5 +DA:120,5 +DA:121,5 +DA:122,5 +LF:25 +LH:25 +BRDA:41,0,0,70 +BRDA:41,0,1,3 +BRDA:91,1,0,0 +BRDA:91,1,1,5 +BRDA:94,2,0,1 +BRDA:94,2,1,4 +BRDA:95,3,0,1 +BRDA:95,3,1,4 +BRDA:96,4,0,1 +BRDA:96,4,1,4 +BRDA:97,5,0,0 +BRDA:97,5,1,5 +BRDA:98,6,0,0 +BRDA:98,6,1,0 +BRDA:107,7,0,0 +BRDA:107,7,1,5 +BRDA:120,8,0,5 +BRDA:120,8,1,4 +BRDA:121,9,0,5 +BRDA:121,9,1,4 +BRDA:16,10,0,44 +BRDA:16,10,1,0 +BRDA:16,11,0,44 +BRDA:16,11,1,44 +BRF:24 +BRH:18 +end_of_record +TN: +SF:src/contexts/notifications/application/listeners/cycle-stage-notification.listener.ts +FN:11,(anonymous_2) +FN:16,(anonymous_3) +FNF:2 +FNH:2 +FNDA:2,(anonymous_2) +FNDA:3,(anonymous_3) +DA:1,3 +DA:2,3 +DA:3,3 +DA:4,3 +DA:5,3 +DA:8,3 +DA:9,2 +DA:12,2 +DA:16,3 +DA:17,3 +DA:19,3 +DA:20,3 +DA:23,2 +DA:26,2 +DA:29,2 +DA:32,1 +DA:36,0 +DA:37,0 +LF:18 +LH:16 +BRDA:20,0,0,2 +BRDA:20,0,1,1 +BRDA:36,1,0,0 +BRDA:36,1,1,0 +BRDA:39,2,0,0 +BRDA:39,2,1,0 +BRDA:16,3,0,3 +BRDA:16,3,1,0 +BRDA:16,4,0,3 +BRDA:16,4,1,3 +BRDA:16,5,0,3 +BRDA:16,5,1,0 +BRDA:16,6,0,3 +BRDA:16,6,1,3 +BRDA:12,7,0,3 +BRDA:12,7,1,0 +BRDA:12,8,0,3 +BRDA:12,8,1,3 +BRF:18 +BRH:11 +end_of_record +TN: +SF:src/contexts/notifications/application/listeners/review-stage-notification.listener.ts +FN:11,(anonymous_2) +FN:16,(anonymous_3) +FNF:2 +FNH:2 +FNDA:2,(anonymous_2) +FNDA:5,(anonymous_3) +DA:1,3 +DA:2,3 +DA:3,3 +DA:4,3 +DA:5,3 +DA:8,3 +DA:9,2 +DA:12,2 +DA:16,3 +DA:17,5 +DA:19,5 +DA:20,5 +DA:23,1 +DA:26,1 +DA:29,1 +DA:33,1 +DA:34,1 +DA:37,1 +DA:41,1 +DA:42,1 +DA:45,1 +DA:49,1 +DA:50,1 +DA:53,1 +DA:56,1 +DA:60,0 +DA:61,0 +LF:27 +LH:25 +BRDA:20,0,0,1 +BRDA:20,0,1,1 +BRDA:20,0,2,1 +BRDA:20,0,3,1 +BRDA:20,0,4,1 +BRDA:60,1,0,0 +BRDA:60,1,1,0 +BRDA:63,2,0,0 +BRDA:63,2,1,0 +BRDA:16,3,0,3 +BRDA:16,3,1,0 +BRDA:16,4,0,3 +BRDA:16,4,1,3 +BRDA:16,5,0,3 +BRDA:16,5,1,0 +BRDA:16,6,0,3 +BRDA:16,6,1,3 +BRDA:12,7,0,3 +BRDA:12,7,1,0 +BRDA:12,8,0,3 +BRDA:12,8,1,3 +BRF:21 +BRH:14 +end_of_record +TN: +SF:src/contexts/notifications/application/listeners/user-created-notification.listener.ts +FN:10,(anonymous_2) +FN:15,(anonymous_3) +FNF:2 +FNH:2 +FNDA:2,(anonymous_2) +FNDA:2,(anonymous_3) +DA:1,3 +DA:2,3 +DA:3,3 +DA:4,3 +DA:7,3 +DA:8,2 +DA:11,2 +DA:15,3 +DA:16,2 +DA:18,2 +DA:19,2 +DA:20,2 +DA:25,0 +DA:26,0 +LF:14 +LH:12 +BRDA:25,0,0,0 +BRDA:25,0,1,0 +BRDA:28,1,0,0 +BRDA:28,1,1,0 +BRDA:15,2,0,3 +BRDA:15,2,1,0 +BRDA:15,3,0,3 +BRDA:15,3,1,3 +BRDA:15,4,0,3 +BRDA:15,4,1,0 +BRDA:15,5,0,3 +BRDA:15,5,1,3 +BRDA:11,6,0,3 +BRDA:11,6,1,0 +BRDA:11,7,0,3 +BRDA:11,7,1,3 +BRF:16 +BRH:9 +end_of_record +TN: +SF:src/contexts/notifications/application/services/review-email-notification.service.ts +FN:43,(anonymous_4) +FN:69,(anonymous_5) +FN:96,(anonymous_6) +FN:125,(anonymous_7) +FN:155,(anonymous_8) +FN:200,(anonymous_9) +FN:233,(anonymous_10) +FN:276,(anonymous_11) +FN:387,(anonymous_12) +FNF:9 +FNH:9 +FNDA:2,(anonymous_4) +FNDA:7,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:3,(anonymous_7) +FNDA:3,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:15,(anonymous_11) +FNDA:15,(anonymous_12) +DA:1,3 +DA:2,3 +DA:3,3 +DA:7,3 +DA:11,3 +DA:15,3 +DA:16,3 +DA:21,3 +DA:25,3 +DA:26,3 +DA:27,3 +DA:33,3 +DA:34,3 +DA:40,3 +DA:41,2 +DA:44,2 +DA:46,2 +DA:48,2 +DA:50,2 +DA:52,2 +DA:54,2 +DA:56,2 +DA:57,2 +DA:58,2 +DA:70,7 +DA:71,7 +DA:72,1 +DA:73,1 +DA:76,6 +DA:88,6 +DA:97,2 +DA:99,2 +DA:101,2 +DA:102,2 +DA:103,0 +DA:104,0 +DA:107,2 +DA:122,2 +DA:126,3 +DA:127,3 +DA:128,1 +DA:129,1 +DA:132,2 +DA:133,2 +DA:134,0 +DA:137,0 +DA:140,2 +DA:152,2 +DA:156,3 +DA:157,3 +DA:158,1 +DA:159,1 +DA:162,2 +DA:167,2 +DA:169,2 +DA:170,2 +DA:171,2 +DA:172,0 +DA:175,0 +DA:178,2 +DA:192,2 +DA:193,2 +DA:197,2 +DA:201,2 +DA:202,2 +DA:203,0 +DA:204,0 +DA:207,2 +DA:208,2 +DA:209,0 +DA:212,0 +DA:215,2 +DA:230,2 +DA:234,2 +DA:235,2 +DA:236,0 +DA:237,0 +DA:240,2 +DA:242,2 +DA:244,2 +DA:245,1 +DA:246,1 +DA:247,0 +DA:250,0 +DA:253,1 +DA:268,1 +DA:269,1 +DA:273,2 +DA:288,15 +DA:290,15 +DA:291,0 +DA:292,0 +DA:294,15 +DA:295,0 +DA:296,0 +DA:298,15 +DA:299,0 +DA:302,0 +DA:305,15 +DA:308,15 +DA:309,7 +DA:314,8 +DA:315,2 +DA:321,6 +DA:327,15 +DA:328,1 +DA:331,1 +DA:335,14 +DA:346,14 +DA:355,14 +DA:356,14 +DA:370,11 +DA:371,11 +DA:374,11 +DA:377,3 +DA:378,3 +DA:382,3 +DA:383,3 +DA:388,15 +DA:389,15 +DA:390,15 +DA:391,15 +LF:122 +LH:102 +BRDA:71,0,0,1 +BRDA:71,0,1,6 +BRDA:88,1,0,3 +BRDA:88,1,1,3 +BRDA:102,2,0,0 +BRDA:102,2,1,2 +BRDA:122,3,0,1 +BRDA:122,3,1,1 +BRDA:127,4,0,1 +BRDA:127,4,1,2 +BRDA:133,5,0,0 +BRDA:133,5,1,2 +BRDA:152,6,0,2 +BRDA:152,6,1,0 +BRDA:157,7,0,1 +BRDA:157,7,1,2 +BRDA:171,8,0,0 +BRDA:171,8,1,2 +BRDA:192,9,0,2 +BRDA:192,9,1,0 +BRDA:202,10,0,0 +BRDA:202,10,1,2 +BRDA:208,11,0,0 +BRDA:208,11,1,2 +BRDA:230,12,0,2 +BRDA:230,12,1,0 +BRDA:235,13,0,0 +BRDA:235,13,1,2 +BRDA:246,14,0,0 +BRDA:246,14,1,1 +BRDA:268,15,0,1 +BRDA:268,15,1,0 +BRDA:290,16,0,0 +BRDA:290,16,1,15 +BRDA:294,17,0,0 +BRDA:294,17,1,15 +BRDA:298,18,0,0 +BRDA:298,18,1,15 +BRDA:308,19,0,7 +BRDA:308,19,1,8 +BRDA:314,20,0,2 +BRDA:314,20,1,6 +BRDA:327,21,0,1 +BRDA:327,21,1,14 +BRDA:335,22,0,14 +BRDA:335,22,1,14 +BRDA:338,23,0,7 +BRDA:338,23,1,7 +BRDA:339,24,0,2 +BRDA:339,24,1,12 +BRDA:377,25,0,3 +BRDA:377,25,1,0 +BRDA:380,26,0,3 +BRDA:380,26,1,0 +BRDA:388,27,0,15 +BRDA:388,27,1,0 +BRDA:389,28,0,15 +BRDA:389,28,1,0 +BRDA:390,29,0,0 +BRDA:390,29,1,15 +BRDA:44,30,0,0 +BRDA:44,30,1,3 +BRDA:44,31,0,3 +BRDA:44,31,1,0 +BRDA:44,32,0,0 +BRDA:44,32,1,3 +BRDA:44,33,0,3 +BRDA:44,33,1,0 +BRDA:46,34,0,0 +BRDA:46,34,1,3 +BRDA:46,35,0,3 +BRDA:46,35,1,0 +BRDA:48,36,0,0 +BRDA:48,36,1,3 +BRDA:48,37,0,3 +BRDA:48,37,1,0 +BRDA:50,38,0,0 +BRDA:50,38,1,3 +BRDA:50,39,0,3 +BRDA:50,39,1,0 +BRDA:52,40,0,0 +BRDA:52,40,1,3 +BRDA:52,41,0,3 +BRDA:52,41,1,0 +BRDA:54,42,0,0 +BRDA:54,42,1,3 +BRDA:54,43,0,3 +BRDA:54,43,1,0 +BRDA:56,44,0,3 +BRDA:56,44,1,0 +BRDA:56,45,0,3 +BRDA:56,45,1,3 +BRDA:57,46,0,3 +BRDA:57,46,1,0 +BRDA:57,47,0,3 +BRDA:57,47,1,3 +BRF:96 +BRH:61 +end_of_record +TN: +SF:src/contexts/notifications/infrastructure/prisma-repositories/notification-log.repository.ts +FN:15,(anonymous_2) +FN:17,(anonymous_3) +FN:35,(anonymous_4) +FN:53,(anonymous_5) +FN:73,(anonymous_6) +FN:80,(anonymous_7) +FN:87,(anonymous_8) +FNF:7 +FNH:7 +FNDA:3,(anonymous_2) +FNDA:12,(anonymous_3) +FNDA:5,(anonymous_4) +FNDA:13,(anonymous_5) +FNDA:29,(anonymous_6) +FNDA:13,(anonymous_7) +FNDA:5,(anonymous_8) +DA:1,3 +DA:2,3 +DA:3,3 +DA:9,3 +DA:12,3 +DA:13,3 +DA:15,3 +DA:22,12 +DA:32,12 +DA:40,5 +DA:50,5 +DA:61,13 +DA:70,13 +DA:74,29 +DA:77,27 +DA:81,13 +DA:88,5 +LF:17 +LH:17 +BRDA:32,0,0,4 +BRDA:32,0,1,8 +BRDA:50,1,0,2 +BRDA:50,1,1,3 +BRDA:70,2,0,6 +BRDA:70,2,1,7 +BRDA:15,3,0,3 +BRDA:15,3,1,0 +BRDA:15,4,0,3 +BRDA:15,4,1,3 +BRF:10 +BRH:9 +end_of_record +TN: +SF:src/contexts/organisation/application/services/position-hierarchy.service.ts +FN:10,(anonymous_4) +FN:16,(anonymous_5) +FN:32,(anonymous_6) +FN:41,(anonymous_7) +FN:48,(anonymous_8) +FN:54,(anonymous_9) +FN:62,(anonymous_10) +FNF:7 +FNH:7 +FNDA:6,(anonymous_4) +FNDA:11,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:2,(anonymous_10) +DA:1,6 +DA:5,6 +DA:6,6 +DA:9,6 +DA:12,6 +DA:13,6 +DA:20,11 +DA:21,1 +DA:26,10 +DA:27,9 +DA:29,8 +DA:36,2 +DA:37,2 +DA:38,2 +DA:44,2 +DA:46,2 +DA:47,2 +DA:49,2 +DA:57,2 +DA:58,2 +DA:61,2 +DA:63,2 +LF:22 +LH:22 +BRDA:20,0,0,1 +BRDA:20,0,1,10 +BRDA:13,1,0,6 +BRDA:13,1,1,0 +BRDA:13,2,0,6 +BRDA:13,2,1,6 +BRF:6 +BRH:5 +end_of_record +TN: +SF:src/contexts/organisation/application/services/position.service.ts +FN:22,(anonymous_4) +FN:29,(anonymous_5) +FN:37,(anonymous_6) +FN:41,(anonymous_7) +FN:47,(anonymous_8) +FN:61,(anonymous_9) +FN:78,(anonymous_10) +FN:82,(anonymous_11) +FNF:8 +FNH:8 +FNDA:50,(anonymous_4) +FNDA:108,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:53,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:4,(anonymous_10) +FNDA:3,(anonymous_11) +DA:6,50 +DA:12,50 +DA:16,50 +DA:18,50 +DA:21,50 +DA:24,50 +DA:26,50 +DA:30,108 +DA:34,108 +DA:38,2 +DA:42,53 +DA:43,53 +DA:44,46 +DA:51,2 +DA:52,1 +DA:58,1 +DA:62,3 +DA:63,2 +DA:64,2 +DA:65,1 +DA:75,1 +DA:79,4 +DA:81,4 +DA:82,4 +LF:24 +LH:24 +BRDA:32,0,0,108 +BRDA:32,0,1,106 +BRDA:43,1,0,7 +BRDA:43,1,1,46 +BRDA:54,2,0,1 +BRDA:54,2,1,0 +BRDA:64,3,0,1 +BRDA:64,3,1,1 +BRDA:26,4,0,0 +BRDA:26,4,1,50 +BRDA:26,5,0,50 +BRDA:26,5,1,0 +BRF:12 +BRH:9 +end_of_record +TN: +SF:src/contexts/organisation/application/services/team.service.ts +FN:21,(anonymous_4) +FN:27,(anonymous_5) +FN:40,(anonymous_6) +FN:44,(anonymous_7) +FN:50,(anonymous_8) +FN:67,(anonymous_9) +FN:82,(anonymous_10) +FN:101,(anonymous_11) +FN:106,(anonymous_12) +FN:116,(anonymous_13) +FNF:10 +FNH:10 +FNDA:6,(anonymous_4) +FNDA:20,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:23,(anonymous_7) +FNDA:3,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:10,(anonymous_10) +FNDA:1,(anonymous_11) +FNDA:4,(anonymous_12) +FNDA:2,(anonymous_13) +DA:7,6 +DA:13,6 +DA:15,6 +DA:17,6 +DA:20,6 +DA:23,6 +DA:24,6 +DA:28,20 +DA:29,2 +DA:32,19 +DA:37,19 +DA:41,2 +DA:45,23 +DA:46,23 +DA:47,19 +DA:51,3 +DA:53,2 +DA:54,2 +DA:57,1 +DA:64,1 +DA:68,3 +DA:69,2 +DA:70,2 +DA:71,1 +DA:79,1 +DA:87,10 +DA:88,9 +DA:90,8 +DA:95,8 +DA:96,1 +DA:98,7 +DA:102,1 +DA:103,1 +DA:110,4 +DA:111,4 +DA:113,4 +DA:115,1 +DA:117,2 +DA:118,2 +DA:121,2 +DA:123,0 +DA:128,1 +LF:42 +LH:41 +BRDA:28,0,0,2 +BRDA:28,0,1,18 +BRDA:28,1,0,20 +BRDA:28,1,1,2 +BRDA:34,2,0,19 +BRDA:34,2,1,19 +BRDA:35,3,0,19 +BRDA:35,3,1,18 +BRDA:46,4,0,4 +BRDA:46,4,1,19 +BRDA:53,5,0,2 +BRDA:53,5,1,0 +BRDA:53,6,0,2 +BRDA:53,6,1,2 +BRDA:59,7,0,1 +BRDA:59,7,1,0 +BRDA:70,8,0,1 +BRDA:70,8,1,1 +BRDA:95,9,0,1 +BRDA:95,9,1,7 +BRDA:113,10,0,3 +BRDA:113,10,1,1 +BRDA:24,11,0,6 +BRDA:24,11,1,0 +BRDA:24,12,0,6 +BRDA:24,12,1,6 +BRF:26 +BRH:23 +end_of_record +TN: +SF:src/contexts/organisation/infrastructure/prisma-repositories/position-hierarchy.repository.ts +FN:10,(anonymous_2) +FN:12,(anonymous_3) +FN:42,(anonymous_4) +FN:51,(anonymous_5) +FN:61,(anonymous_6) +FNF:5 +FNH:5 +FNDA:6,(anonymous_2) +FNDA:14,(anonymous_3) +FNDA:4,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:4,(anonymous_6) +DA:1,6 +DA:2,6 +DA:3,6 +DA:6,6 +DA:9,6 +DA:10,6 +DA:16,14 +DA:17,14 +DA:20,12 +DA:22,2 +DA:26,2 +DA:36,2 +DA:38,0 +DA:46,4 +DA:54,4 +DA:58,4 +DA:64,4 +DA:68,4 +LF:18 +LH:17 +BRDA:22,0,0,2 +BRDA:22,0,1,0 +BRDA:23,1,0,2 +BRDA:23,1,1,2 +BRDA:36,2,0,2 +BRDA:36,2,1,0 +BRDA:10,3,0,6 +BRDA:10,3,1,0 +BRDA:10,4,0,6 +BRDA:10,4,1,6 +BRF:10 +BRH:7 +end_of_record +TN: +SF:src/contexts/organisation/infrastructure/prisma-repositories/position.repository.ts +FN:16,(anonymous_2) +FN:18,(anonymous_3) +FN:25,(anonymous_4) +FN:30,(anonymous_5) +FN:42,(anonymous_6) +FN:53,(anonymous_7) +FN:57,(anonymous_8) +FN:87,(anonymous_9) +FNF:8 +FNH:8 +FNDA:53,(anonymous_2) +FNDA:365,(anonymous_3) +FNDA:55,(anonymous_4) +FNDA:7,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:7,(anonymous_8) +FNDA:7,(anonymous_9) +DA:2,53 +DA:8,53 +DA:9,53 +DA:12,53 +DA:15,53 +DA:16,53 +DA:19,365 +DA:22,365 +DA:26,55 +DA:27,55 +DA:31,7 +DA:32,7 +DA:34,7 +DA:39,7 +DA:46,2 +DA:50,2 +DA:54,2 +DA:58,7 +DA:59,7 +DA:90,7 +DA:91,7 +DA:92,7 +LF:22 +LH:22 +BRDA:27,0,0,47 +BRDA:27,0,1,8 +BRDA:60,1,0,0 +BRDA:60,1,1,7 +BRDA:73,2,0,2 +BRDA:73,2,1,5 +BRDA:76,3,0,0 +BRDA:76,3,1,7 +BRDA:90,4,0,7 +BRDA:90,4,1,6 +BRDA:91,5,0,7 +BRDA:91,5,1,6 +BRDA:16,6,0,53 +BRDA:16,6,1,0 +BRDA:16,7,0,53 +BRDA:16,7,1,53 +BRF:16 +BRH:13 +end_of_record +TN: +SF:src/contexts/organisation/infrastructure/prisma-repositories/team.repository.ts +FN:18,(anonymous_2) +FN:20,(anonymous_3) +FN:27,(anonymous_4) +FN:32,(anonymous_5) +FN:41,(anonymous_6) +FN:52,(anonymous_7) +FN:56,(anonymous_8) +FN:90,(anonymous_9) +FN:96,(anonymous_10) +FN:104,(anonymous_11) +FN:135,(anonymous_12) +FNF:11 +FNH:11 +FNDA:41,(anonymous_2) +FNDA:40,(anonymous_3) +FNDA:25,(anonymous_4) +FNDA:5,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:15,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:5,(anonymous_10) +FNDA:5,(anonymous_11) +FNDA:5,(anonymous_12) +DA:1,41 +DA:2,41 +DA:8,41 +DA:9,41 +DA:13,41 +DA:14,41 +DA:17,41 +DA:18,41 +DA:21,40 +DA:24,40 +DA:28,25 +DA:29,25 +DA:33,5 +DA:34,5 +DA:36,5 +DA:38,5 +DA:45,2 +DA:49,2 +DA:53,2 +DA:61,15 +DA:62,15 +DA:69,14 +DA:71,1 +DA:75,1 +DA:84,1 +DA:86,0 +DA:91,2 +DA:97,5 +DA:101,5 +DA:105,5 +DA:106,5 +DA:138,5 +DA:139,5 +DA:140,5 +LF:34 +LH:33 +BRDA:29,0,0,20 +BRDA:29,0,1,5 +BRDA:66,1,0,15 +BRDA:66,1,1,1 +BRDA:71,2,0,1 +BRDA:71,2,1,0 +BRDA:72,3,0,1 +BRDA:72,3,1,1 +BRDA:80,4,0,1 +BRDA:80,4,1,0 +BRDA:84,5,0,1 +BRDA:84,5,1,0 +BRDA:107,6,0,1 +BRDA:107,6,1,4 +BRDA:110,7,0,0 +BRDA:110,7,1,5 +BRDA:118,8,0,1 +BRDA:118,8,1,4 +BRDA:119,9,0,0 +BRDA:119,9,1,5 +BRDA:138,10,0,5 +BRDA:138,10,1,4 +BRDA:139,11,0,5 +BRDA:139,11,1,4 +BRDA:18,12,0,41 +BRDA:18,12,1,0 +BRDA:18,13,0,41 +BRDA:18,13,1,41 +BRF:28 +BRH:22 +end_of_record +TN: +SF:src/contexts/reporting/application/listeners/cycle-stage.listener.ts +FN:21,(anonymous_4) +FN:36,(anonymous_5) +FN:47,(anonymous_6) +FNF:3 +FNH:2 +FNDA:17,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:0,(anonymous_6) +DA:1,17 +DA:2,17 +DA:3,17 +DA:4,17 +DA:5,17 +DA:6,17 +DA:10,17 +DA:18,17 +DA:19,17 +DA:22,17 +DA:23,17 +DA:25,17 +DA:36,17 +DA:39,2 +DA:40,1 +DA:43,1 +DA:47,1 +DA:48,0 +DA:49,0 +DA:50,0 +DA:56,1 +DA:60,1 +DA:61,1 +DA:67,1 +DA:71,1 +DA:75,1 +DA:80,0 +LF:27 +LH:23 +BRDA:39,0,0,1 +BRDA:39,0,1,1 +BRDA:49,1,0,0 +BRDA:49,1,1,0 +BRDA:37,2,0,17 +BRDA:37,2,1,0 +BRDA:37,3,0,17 +BRDA:37,3,1,17 +BRDA:38,4,0,17 +BRDA:38,4,1,0 +BRDA:38,5,0,17 +BRDA:38,5,1,17 +BRDA:22,6,0,17 +BRDA:22,6,1,0 +BRDA:22,7,0,17 +BRDA:22,7,1,17 +BRDA:22,8,0,17 +BRDA:22,8,1,0 +BRDA:22,9,0,17 +BRDA:22,9,1,17 +BRDA:23,10,0,0 +BRDA:23,10,1,17 +BRDA:23,11,0,17 +BRDA:23,11,1,0 +BRF:24 +BRH:16 +end_of_record +TN: +SF:src/contexts/reporting/application/listeners/review-stage.listener.ts +FN:17,(anonymous_2) +FN:30,(anonymous_3) +FNF:2 +FNH:2 +FNDA:17,(anonymous_2) +FNDA:2,(anonymous_3) +DA:1,17 +DA:2,17 +DA:3,17 +DA:4,17 +DA:5,17 +DA:6,17 +DA:14,17 +DA:15,17 +DA:18,17 +DA:19,17 +DA:30,17 +DA:33,2 +DA:34,1 +DA:37,1 +DA:40,1 +DA:44,1 +DA:45,1 +DA:50,1 +DA:54,1 +DA:58,1 +DA:63,0 +LF:21 +LH:20 +BRDA:33,0,0,1 +BRDA:33,0,1,1 +BRDA:31,1,0,17 +BRDA:31,1,1,0 +BRDA:31,2,0,17 +BRDA:31,2,1,17 +BRDA:32,3,0,17 +BRDA:32,3,1,0 +BRDA:32,4,0,17 +BRDA:32,4,1,17 +BRDA:18,5,0,17 +BRDA:18,5,1,0 +BRDA:18,6,0,17 +BRDA:18,6,1,17 +BRDA:18,7,0,17 +BRDA:18,7,1,0 +BRDA:18,8,0,17 +BRDA:18,8,1,17 +BRF:18 +BRH:14 +end_of_record +TN: +SF:src/contexts/reporting/application/services/report-analytics.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:35,(anonymous_6) +FNF:3 +FNH:3 +FNDA:17,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:2,(anonymous_6) +DA:1,17 +DA:4,17 +DA:8,17 +DA:11,17 +DA:14,17 +DA:15,17 +DA:22,4 +DA:24,2 +DA:26,2 +DA:27,0 +DA:32,2 +DA:39,2 +DA:41,2 +DA:42,1 +DA:47,1 +DA:49,1 +LF:16 +LH:15 +BRDA:26,0,0,0 +BRDA:26,0,1,2 +BRDA:41,1,0,1 +BRDA:41,1,1,1 +BRDA:14,2,0,0 +BRDA:14,2,1,17 +BRDA:14,3,0,17 +BRDA:14,3,1,0 +BRDA:14,4,0,17 +BRDA:14,4,1,0 +BRDA:14,5,0,17 +BRDA:14,5,1,17 +BRF:12 +BRH:8 +end_of_record +TN: +SF:src/contexts/reporting/application/services/report-comment.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:35,(anonymous_6) +FN:47,(anonymous_7) +FNF:4 +FNH:4 +FNDA:17,(anonymous_4) +FNDA:3,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:4,(anonymous_7) +DA:1,17 +DA:4,17 +DA:8,17 +DA:11,17 +DA:14,17 +DA:15,17 +DA:22,3 +DA:24,2 +DA:26,2 +DA:27,1 +DA:32,1 +DA:36,2 +DA:38,2 +DA:39,1 +DA:44,1 +DA:48,4 +LF:16 +LH:16 +BRDA:26,0,0,1 +BRDA:26,0,1,1 +BRDA:26,1,0,2 +BRDA:26,1,1,2 +BRDA:38,2,0,1 +BRDA:38,2,1,1 +BRDA:14,3,0,0 +BRDA:14,3,1,17 +BRDA:14,4,0,17 +BRDA:14,4,1,0 +BRDA:14,5,0,17 +BRDA:14,5,1,0 +BRDA:14,6,0,17 +BRDA:14,6,1,17 +BRF:14 +BRH:11 +end_of_record +TN: +SF:src/contexts/reporting/application/services/report-insight.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:35,(anonymous_6) +FNF:3 +FNH:3 +FNDA:17,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:2,(anonymous_6) +DA:1,17 +DA:4,17 +DA:8,17 +DA:11,17 +DA:14,17 +DA:15,17 +DA:22,4 +DA:24,2 +DA:26,2 +DA:27,0 +DA:32,2 +DA:39,2 +DA:41,2 +DA:42,1 +DA:47,1 +DA:49,1 +LF:16 +LH:15 +BRDA:26,0,0,0 +BRDA:26,0,1,2 +BRDA:41,1,0,1 +BRDA:41,1,1,1 +BRDA:14,2,0,0 +BRDA:14,2,1,17 +BRDA:14,3,0,17 +BRDA:14,3,1,0 +BRDA:14,4,0,17 +BRDA:14,4,1,0 +BRDA:14,5,0,17 +BRDA:14,5,1,17 +BRF:12 +BRH:8 +end_of_record +TN: +SF:src/contexts/reporting/application/services/reports.service.ts +FN:87,(anonymous_5) +FN:116,(anonymous_6) +FN:128,(anonymous_7) +FN:142,(anonymous_8) +FN:159,(anonymous_9) +FN:194,(anonymous_10) +FN:216,(anonymous_11) +FN:238,(anonymous_12) +FN:240,(anonymous_13) +FN:250,(anonymous_14) +FN:259,(anonymous_15) +FN:387,(anonymous_16) +FN:418,(anonymous_17) +FN:431,(anonymous_18) +FN:445,(anonymous_19) +FN:528,(anonymous_20) +FN:541,(anonymous_21) +FN:568,(anonymous_22) +FN:572,(anonymous_23) +FN:573,(anonymous_24) +FN:576,(anonymous_25) +FN:587,(anonymous_26) +FN:604,(anonymous_27) +FN:610,(anonymous_28) +FN:635,(anonymous_29) +FN:646,(anonymous_30) +FN:766,(anonymous_31) +FN:769,(anonymous_32) +FN:772,(anonymous_33) +FN:857,(anonymous_34) +FN:864,(anonymous_35) +FN:867,(anonymous_36) +FN:887,(anonymous_37) +FN:893,(anonymous_38) +FN:898,(anonymous_39) +FN:909,(anonymous_40) +FN:925,(anonymous_41) +FN:930,(anonymous_42) +FN:933,(anonymous_43) +FN:936,(anonymous_44) +FN:981,(anonymous_45) +FN:996,(anonymous_46) +FN:1009,(anonymous_47) +FN:1062,(anonymous_48) +FN:1068,(anonymous_49) +FN:1079,(anonymous_50) +FN:1107,(anonymous_51) +FN:1108,(anonymous_52) +FN:1110,(anonymous_53) +FN:1111,(anonymous_54) +FN:1125,(anonymous_55) +FN:1131,(anonymous_56) +FN:1137,(anonymous_57) +FN:1143,(anonymous_58) +FN:1271,(anonymous_59) +FN:1273,(anonymous_60) +FN:1284,(anonymous_61) +FNF:57 +FNH:49 +FNDA:17,(anonymous_5) +FNDA:3,(anonymous_6) +FNDA:3,(anonymous_7) +FNDA:17,(anonymous_8) +FNDA:14,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:4,(anonymous_11) +FNDA:2,(anonymous_12) +FNDA:1,(anonymous_13) +FNDA:2,(anonymous_14) +FNDA:2,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:2,(anonymous_19) +FNDA:1,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:25,(anonymous_22) +FNDA:13,(anonymous_23) +FNDA:12,(anonymous_24) +FNDA:12,(anonymous_25) +FNDA:41,(anonymous_26) +FNDA:4,(anonymous_27) +FNDA:4,(anonymous_28) +FNDA:2,(anonymous_29) +FNDA:1,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:1,(anonymous_32) +FNDA:1,(anonymous_33) +FNDA:2,(anonymous_34) +FNDA:1,(anonymous_35) +FNDA:1,(anonymous_36) +FNDA:3,(anonymous_37) +FNDA:6,(anonymous_38) +FNDA:2,(anonymous_39) +FNDA:16,(anonymous_40) +FNDA:2,(anonymous_41) +FNDA:1,(anonymous_42) +FNDA:1,(anonymous_43) +FNDA:1,(anonymous_44) +FNDA:24,(anonymous_45) +FNDA:48,(anonymous_46) +FNDA:4,(anonymous_47) +FNDA:4,(anonymous_48) +FNDA:0,(anonymous_49) +FNDA:2,(anonymous_50) +FNDA:2,(anonymous_51) +FNDA:2,(anonymous_52) +FNDA:2,(anonymous_53) +FNDA:2,(anonymous_54) +FNDA:2,(anonymous_55) +FNDA:2,(anonymous_56) +FNDA:0,(anonymous_57) +FNDA:0,(anonymous_58) +FNDA:6,(anonymous_59) +FNDA:12,(anonymous_60) +FNDA:4,(anonymous_61) +DA:1,17 +DA:16,17 +DA:23,17 +DA:24,17 +DA:28,17 +DA:33,17 +DA:37,17 +DA:41,17 +DA:45,17 +DA:49,17 +DA:54,17 +DA:58,17 +DA:63,17 +DA:64,17 +DA:65,17 +DA:66,17 +DA:70,17 +DA:74,17 +DA:78,17 +DA:84,17 +DA:85,17 +DA:89,17 +DA:91,17 +DA:93,17 +DA:95,17 +DA:97,17 +DA:99,17 +DA:101,17 +DA:103,17 +DA:105,17 +DA:107,17 +DA:109,17 +DA:111,17 +DA:114,17 +DA:120,3 +DA:121,2 +DA:129,3 +DA:132,2 +DA:135,2 +DA:136,1 +DA:143,17 +DA:145,17 +DA:146,4 +DA:149,13 +DA:151,10 +DA:163,14 +DA:166,14 +DA:169,14 +DA:171,4 +DA:173,4 +DA:174,0 +DA:178,4 +DA:179,4 +DA:181,4 +DA:183,3 +DA:187,3 +DA:189,3 +DA:198,2 +DA:199,2 +DA:200,1 +DA:205,1 +DA:207,1 +DA:217,4 +DA:219,4 +DA:220,4 +DA:221,1 +DA:222,1 +DA:225,3 +DA:226,3 +DA:227,1 +DA:230,2 +DA:234,2 +DA:235,2 +DA:238,2 +DA:241,1 +DA:247,2 +DA:249,2 +DA:250,2 +DA:251,2 +DA:254,2 +DA:255,0 +DA:258,2 +DA:260,2 +DA:261,2 +DA:266,2 +DA:271,2 +DA:277,2 +DA:281,2 +DA:286,2 +DA:291,2 +DA:293,2 +DA:295,2 +DA:300,2 +DA:329,2 +DA:331,2 +DA:336,2 +DA:343,2 +DA:347,2 +DA:353,2 +DA:359,2 +DA:364,2 +DA:368,2 +DA:372,2 +DA:376,2 +DA:396,0 +DA:397,0 +DA:398,0 +DA:400,0 +DA:406,0 +DA:408,0 +DA:413,0 +DA:418,0 +DA:419,0 +DA:422,0 +DA:425,0 +DA:431,0 +DA:432,0 +DA:451,2 +DA:452,2 +DA:453,1 +DA:458,1 +DA:465,2 +DA:466,2 +DA:467,2 +DA:470,0 +DA:471,2 +DA:472,2 +DA:473,2 +DA:477,0 +DA:478,2 +DA:479,2 +DA:482,2 +DA:483,1 +DA:484,1 +DA:486,1 +DA:490,1 +DA:491,1 +DA:493,1 +DA:494,1 +DA:496,0 +DA:500,0 +DA:501,0 +DA:504,0 +DA:507,0 +DA:509,0 +DA:532,1 +DA:545,0 +DA:546,0 +DA:547,0 +DA:548,0 +DA:551,0 +DA:552,0 +DA:555,0 +DA:556,0 +DA:559,0 +DA:571,25 +DA:572,13 +DA:573,12 +DA:574,25 +DA:575,11 +DA:576,12 +DA:579,11 +DA:590,41 +DA:592,33 +DA:593,33 +DA:609,4 +DA:610,4 +DA:613,4 +DA:614,2 +DA:617,2 +DA:618,0 +DA:621,2 +DA:625,2 +DA:645,2 +DA:646,1 +DA:649,2 +DA:653,2 +DA:654,2 +DA:655,2 +DA:656,2 +DA:657,2 +DA:659,2 +DA:661,1 +DA:663,1 +DA:667,1 +DA:671,1 +DA:676,1 +DA:677,0 +DA:679,1 +DA:680,1 +DA:682,1 +DA:683,1 +DA:686,1 +DA:690,1 +DA:694,1 +DA:699,1 +DA:703,1 +DA:708,1 +DA:733,1 +DA:734,1 +DA:735,1 +DA:745,1 +DA:746,0 +DA:748,1 +DA:749,1 +DA:751,1 +DA:752,1 +DA:755,1 +DA:759,2 +DA:760,2 +DA:761,2 +DA:762,2 +DA:764,2 +DA:765,1 +DA:766,0 +DA:768,1 +DA:769,1 +DA:771,1 +DA:772,1 +DA:775,1 +DA:776,0 +DA:778,1 +DA:779,1 +DA:781,1 +DA:782,1 +DA:785,1 +DA:789,1 +DA:793,1 +DA:798,1 +DA:802,1 +DA:807,1 +DA:833,2 +DA:861,2 +DA:865,1 +DA:867,1 +DA:869,2 +DA:870,2 +DA:871,2 +DA:872,0 +DA:874,2 +DA:875,2 +DA:876,2 +DA:878,2 +DA:891,3 +DA:894,6 +DA:898,2 +DA:899,3 +DA:913,16 +DA:914,16 +DA:916,0 +DA:929,2 +DA:930,1 +DA:932,2 +DA:933,1 +DA:935,2 +DA:936,1 +DA:939,2 +DA:943,2 +DA:947,2 +DA:952,2 +DA:956,2 +DA:960,2 +DA:985,24 +DA:986,14 +DA:988,10 +DA:997,48 +DA:998,12 +DA:1015,4 +DA:1016,4 +DA:1017,4 +DA:1019,4 +DA:1023,4 +DA:1027,4 +DA:1032,4 +DA:1036,4 +DA:1041,4 +DA:1067,4 +DA:1070,0 +DA:1074,0 +DA:1077,0 +DA:1080,2 +DA:1106,4 +DA:1107,2 +DA:1108,2 +DA:1109,4 +DA:1110,2 +DA:1111,2 +DA:1114,4 +DA:1116,4 +DA:1118,4 +DA:1120,4 +DA:1122,4 +DA:1126,2 +DA:1132,2 +DA:1138,0 +DA:1144,0 +DA:1149,4 +DA:1151,4 +DA:1179,4 +DA:1207,4 +DA:1235,4 +DA:1263,4 +DA:1271,17 +DA:1272,6 +DA:1273,12 +DA:1275,6 +DA:1276,4 +DA:1285,4 +DA:1286,4 +DA:1287,8 +DA:1289,4 +LF:311 +LH:267 +BRDA:129,0,0,1 +BRDA:129,0,1,2 +BRDA:132,1,0,2 +BRDA:132,1,1,2 +BRDA:135,2,0,1 +BRDA:135,2,1,1 +BRDA:145,3,0,4 +BRDA:145,3,1,13 +BRDA:163,4,0,0 +BRDA:163,4,1,14 +BRDA:166,5,0,14 +BRDA:166,5,1,14 +BRDA:169,6,0,10 +BRDA:169,6,1,4 +BRDA:173,7,0,0 +BRDA:173,7,1,4 +BRDA:181,8,0,1 +BRDA:181,8,1,3 +BRDA:181,9,0,4 +BRDA:181,9,1,4 +BRDA:187,10,0,0 +BRDA:187,10,1,3 +BRDA:199,11,0,1 +BRDA:199,11,1,1 +BRDA:220,12,0,1 +BRDA:220,12,1,3 +BRDA:226,13,0,1 +BRDA:226,13,1,2 +BRDA:254,14,0,0 +BRDA:254,14,1,2 +BRDA:268,15,0,2 +BRDA:268,15,1,1 +BRDA:273,16,0,2 +BRDA:273,16,1,1 +BRDA:396,17,0,0 +BRDA:396,17,1,0 +BRDA:408,18,0,0 +BRDA:408,18,1,0 +BRDA:409,19,0,0 +BRDA:409,19,1,0 +BRDA:419,20,0,0 +BRDA:419,20,1,0 +BRDA:431,21,0,0 +BRDA:431,21,1,0 +BRDA:453,22,0,1 +BRDA:453,22,1,0 +BRDA:454,23,0,1 +BRDA:454,23,1,1 +BRDA:454,23,2,1 +BRDA:467,24,0,0 +BRDA:467,24,1,2 +BRDA:472,25,0,0 +BRDA:472,25,1,2 +BRDA:473,26,0,0 +BRDA:473,26,1,2 +BRDA:474,27,0,2 +BRDA:474,27,1,2 +BRDA:478,28,0,2 +BRDA:478,28,1,1 +BRDA:484,29,0,0 +BRDA:484,29,1,1 +BRDA:484,30,0,1 +BRDA:484,30,1,1 +BRDA:487,31,0,1 +BRDA:487,31,1,0 +BRDA:488,32,0,1 +BRDA:488,32,1,0 +BRDA:491,33,0,0 +BRDA:491,33,1,1 +BRDA:494,34,0,1 +BRDA:494,34,1,0 +BRDA:500,35,0,0 +BRDA:500,35,1,0 +BRDA:507,36,0,0 +BRDA:507,36,1,0 +BRDA:511,37,0,0 +BRDA:511,37,1,0 +BRDA:552,38,0,0 +BRDA:552,38,1,0 +BRDA:555,39,0,0 +BRDA:555,39,1,0 +BRDA:555,40,0,0 +BRDA:555,40,1,0 +BRDA:572,41,0,13 +BRDA:572,41,1,12 +BRDA:573,42,0,12 +BRDA:573,42,1,0 +BRDA:574,43,0,14 +BRDA:574,43,1,11 +BRDA:590,44,0,8 +BRDA:590,44,1,33 +BRDA:590,45,0,41 +BRDA:590,45,1,33 +BRDA:592,46,0,33 +BRDA:592,46,1,0 +BRDA:613,47,0,2 +BRDA:613,47,1,2 +BRDA:617,48,0,0 +BRDA:617,48,1,2 +BRDA:661,49,0,1 +BRDA:661,49,1,0 +BRDA:676,50,0,0 +BRDA:676,50,1,1 +BRDA:679,51,0,1 +BRDA:679,51,1,0 +BRDA:682,52,0,1 +BRDA:682,52,1,0 +BRDA:717,53,0,1 +BRDA:717,53,1,1 +BRDA:718,54,0,1 +BRDA:718,54,1,0 +BRDA:719,55,0,1 +BRDA:719,55,1,0 +BRDA:721,56,0,1 +BRDA:721,56,1,1 +BRDA:723,57,0,1 +BRDA:723,57,1,0 +BRDA:725,58,0,1 +BRDA:725,58,1,0 +BRDA:727,59,0,1 +BRDA:727,59,1,1 +BRDA:729,60,0,1 +BRDA:729,60,1,1 +BRDA:734,61,0,1 +BRDA:734,61,1,0 +BRDA:734,62,0,1 +BRDA:734,62,1,1 +BRDA:735,63,0,1 +BRDA:735,63,1,1 +BRDA:739,64,0,1 +BRDA:739,64,1,0 +BRDA:745,65,0,0 +BRDA:745,65,1,1 +BRDA:748,66,0,1 +BRDA:748,66,1,0 +BRDA:751,67,0,1 +BRDA:751,67,1,0 +BRDA:775,68,0,0 +BRDA:775,68,1,1 +BRDA:778,69,0,1 +BRDA:778,69,1,0 +BRDA:781,70,0,1 +BRDA:781,70,1,0 +BRDA:816,71,0,1 +BRDA:816,71,1,1 +BRDA:817,72,0,1 +BRDA:817,72,1,0 +BRDA:818,73,0,1 +BRDA:818,73,1,0 +BRDA:820,74,0,1 +BRDA:820,74,1,1 +BRDA:822,75,0,1 +BRDA:822,75,1,0 +BRDA:824,76,0,1 +BRDA:824,76,1,0 +BRDA:826,77,0,1 +BRDA:826,77,1,1 +BRDA:828,78,0,1 +BRDA:828,78,1,1 +BRDA:871,79,0,0 +BRDA:871,79,1,2 +BRDA:874,80,0,2 +BRDA:874,80,1,1 +BRDA:894,81,0,6 +BRDA:894,81,1,2 +BRDA:894,81,2,2 +BRDA:913,82,0,16 +BRDA:913,82,1,0 +BRDA:913,83,0,16 +BRDA:913,83,1,10 +BRDA:985,84,0,14 +BRDA:985,84,1,10 +BRDA:985,85,0,24 +BRDA:985,85,1,10 +BRDA:997,86,0,36 +BRDA:997,86,1,12 +BRDA:1070,87,0,0 +BRDA:1070,87,1,0 +BRDA:1071,88,0,0 +BRDA:1071,88,1,0 +BRDA:1072,89,0,0 +BRDA:1072,89,1,0 +BRDA:1074,90,0,0 +BRDA:1074,90,1,0 +BRDA:1075,91,0,0 +BRDA:1075,91,1,0 +BRDA:1076,92,0,0 +BRDA:1076,92,1,0 +BRDA:1082,93,0,1 +BRDA:1082,93,1,1 +BRDA:1087,94,0,2 +BRDA:1087,94,1,0 +BRDA:1087,95,0,1 +BRDA:1087,95,1,1 +BRDA:1108,96,0,2 +BRDA:1108,96,1,2 +BRDA:1111,97,0,2 +BRDA:1111,97,1,2 +BRDA:1114,98,0,2 +BRDA:1114,98,1,2 +BRDA:1116,99,0,2 +BRDA:1116,99,1,2 +BRDA:1118,100,0,0 +BRDA:1118,100,1,4 +BRDA:1120,101,0,0 +BRDA:1120,101,1,4 +BRDA:1124,102,0,2 +BRDA:1124,102,1,2 +BRDA:1126,103,0,2 +BRDA:1126,103,1,0 +BRDA:1130,104,0,2 +BRDA:1130,104,1,2 +BRDA:1132,105,0,2 +BRDA:1132,105,1,0 +BRDA:1136,106,0,0 +BRDA:1136,106,1,4 +BRDA:1138,107,0,0 +BRDA:1138,107,1,0 +BRDA:1142,108,0,0 +BRDA:1142,108,1,4 +BRDA:1144,109,0,0 +BRDA:1144,109,1,0 +BRDA:1157,110,0,2 +BRDA:1157,110,1,2 +BRDA:1158,111,0,2 +BRDA:1158,111,1,1 +BRDA:1161,112,0,2 +BRDA:1161,112,1,2 +BRDA:1162,113,0,2 +BRDA:1162,113,1,1 +BRDA:1165,114,0,2 +BRDA:1165,114,1,2 +BRDA:1166,115,0,2 +BRDA:1166,115,1,1 +BRDA:1169,116,0,2 +BRDA:1169,116,1,2 +BRDA:1170,117,0,2 +BRDA:1170,117,1,1 +BRDA:1185,118,0,2 +BRDA:1185,118,1,2 +BRDA:1186,119,0,2 +BRDA:1186,119,1,1 +BRDA:1189,120,0,2 +BRDA:1189,120,1,2 +BRDA:1190,121,0,2 +BRDA:1190,121,1,1 +BRDA:1193,122,0,2 +BRDA:1193,122,1,2 +BRDA:1194,123,0,2 +BRDA:1194,123,1,1 +BRDA:1197,124,0,2 +BRDA:1197,124,1,2 +BRDA:1198,125,0,2 +BRDA:1198,125,1,1 +BRDA:1213,126,0,2 +BRDA:1213,126,1,2 +BRDA:1214,127,0,2 +BRDA:1214,127,1,2 +BRDA:1217,128,0,2 +BRDA:1217,128,1,2 +BRDA:1218,129,0,2 +BRDA:1218,129,1,2 +BRDA:1221,130,0,2 +BRDA:1221,130,1,2 +BRDA:1222,131,0,2 +BRDA:1222,131,1,2 +BRDA:1225,132,0,2 +BRDA:1225,132,1,2 +BRDA:1226,133,0,2 +BRDA:1226,133,1,2 +BRDA:1241,134,0,2 +BRDA:1241,134,1,2 +BRDA:1242,135,0,2 +BRDA:1242,135,1,2 +BRDA:1245,136,0,2 +BRDA:1245,136,1,2 +BRDA:1246,137,0,2 +BRDA:1246,137,1,2 +BRDA:1249,138,0,2 +BRDA:1249,138,1,2 +BRDA:1250,139,0,2 +BRDA:1250,139,1,2 +BRDA:1253,140,0,2 +BRDA:1253,140,1,2 +BRDA:1254,141,0,2 +BRDA:1254,141,1,2 +BRDA:1273,142,0,12 +BRDA:1273,142,1,8 +BRDA:1275,143,0,2 +BRDA:1275,143,1,4 +BRDA:89,144,0,0 +BRDA:89,144,1,17 +BRDA:89,145,0,17 +BRDA:89,145,1,0 +BRDA:89,146,0,0 +BRDA:89,146,1,17 +BRDA:89,147,0,17 +BRDA:89,147,1,0 +BRDA:91,148,0,0 +BRDA:91,148,1,17 +BRDA:91,149,0,17 +BRDA:91,149,1,0 +BRDA:93,150,0,0 +BRDA:93,150,1,17 +BRDA:93,151,0,17 +BRDA:93,151,1,0 +BRDA:95,152,0,0 +BRDA:95,152,1,17 +BRDA:95,153,0,17 +BRDA:95,153,1,0 +BRDA:97,154,0,0 +BRDA:97,154,1,17 +BRDA:97,155,0,17 +BRDA:97,155,1,0 +BRDA:99,156,0,0 +BRDA:99,156,1,17 +BRDA:99,157,0,17 +BRDA:99,157,1,0 +BRDA:101,158,0,0 +BRDA:101,158,1,17 +BRDA:101,159,0,17 +BRDA:101,159,1,0 +BRDA:103,160,0,0 +BRDA:103,160,1,17 +BRDA:103,161,0,17 +BRDA:103,161,1,0 +BRDA:105,162,0,0 +BRDA:105,162,1,17 +BRDA:105,163,0,17 +BRDA:105,163,1,0 +BRDA:107,164,0,0 +BRDA:107,164,1,17 +BRDA:107,165,0,17 +BRDA:107,165,1,0 +BRDA:109,166,0,0 +BRDA:109,166,1,17 +BRDA:109,167,0,17 +BRDA:109,167,1,0 +BRF:338 +BRH:231 +end_of_record +TN: +SF:src/contexts/reporting/application/services/startegic-report-insight.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:36,(anonymous_6) +FNF:3 +FNH:3 +FNDA:17,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:2,(anonymous_6) +DA:1,17 +DA:4,17 +DA:8,17 +DA:11,17 +DA:14,17 +DA:15,17 +DA:22,4 +DA:25,2 +DA:27,2 +DA:28,0 +DA:33,2 +DA:40,2 +DA:42,2 +DA:43,1 +DA:48,1 +DA:50,1 +LF:16 +LH:15 +BRDA:27,0,0,0 +BRDA:27,0,1,2 +BRDA:42,1,0,1 +BRDA:42,1,1,1 +BRDA:14,2,0,0 +BRDA:14,2,1,17 +BRDA:14,3,0,17 +BRDA:14,3,1,0 +BRDA:14,4,0,17 +BRDA:14,4,1,0 +BRDA:14,5,0,17 +BRDA:14,5,1,17 +BRF:12 +BRH:8 +end_of_record +TN: +SF:src/contexts/reporting/application/services/strategic-report-analytics.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:38,(anonymous_6) +FNF:3 +FNH:3 +FNDA:17,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:2,(anonymous_6) +DA:1,17 +DA:4,17 +DA:8,17 +DA:11,17 +DA:14,17 +DA:15,17 +DA:22,4 +DA:25,2 +DA:29,2 +DA:30,0 +DA:35,2 +DA:42,2 +DA:44,2 +DA:45,1 +DA:50,1 +DA:55,1 +LF:16 +LH:15 +BRDA:29,0,0,0 +BRDA:29,0,1,2 +BRDA:44,1,0,1 +BRDA:44,1,1,1 +BRDA:14,2,0,0 +BRDA:14,2,1,17 +BRDA:14,3,0,17 +BRDA:14,3,1,0 +BRDA:14,4,0,17 +BRDA:14,4,1,0 +BRDA:14,5,0,17 +BRDA:14,5,1,17 +BRF:12 +BRH:8 +end_of_record +TN: +SF:src/contexts/reporting/application/services/strategic-reports.service.ts +FN:98,(anonymous_5) +FN:131,(anonymous_6) +FN:143,(anonymous_7) +FN:157,(anonymous_8) +FN:179,(anonymous_9) +FN:199,(anonymous_10) +FN:209,(anonymous_11) +FN:230,(anonymous_12) +FN:319,(anonymous_13) +FN:349,(anonymous_14) +FN:362,(anonymous_15) +FN:375,(anonymous_16) +FN:382,(anonymous_17) +FN:414,(anonymous_18) +FN:418,(anonymous_19) +FN:441,(anonymous_20) +FN:442,(anonymous_21) +FN:444,(anonymous_22) +FN:445,(anonymous_23) +FN:447,(anonymous_24) +FN:451,(anonymous_25) +FN:452,(anonymous_26) +FN:453,(anonymous_27) +FN:455,(anonymous_28) +FN:458,(anonymous_29) +FN:461,(anonymous_30) +FN:465,(anonymous_31) +FN:468,(anonymous_32) +FN:471,(anonymous_33) +FN:553,(anonymous_34) +FN:557,(anonymous_35) +FN:558,(anonymous_36) +FN:561,(anonymous_37) +FN:572,(anonymous_38) +FN:588,(anonymous_39) +FN:609,(anonymous_40) +FN:621,(anonymous_41) +FN:622,(anonymous_42) +FN:630,(anonymous_43) +FN:633,(anonymous_44) +FN:636,(anonymous_45) +FN:693,(anonymous_46) +FN:698,(anonymous_47) +FN:701,(anonymous_48) +FN:704,(anonymous_49) +FN:749,(anonymous_50) +FN:766,(anonymous_51) +FN:782,(anonymous_52) +FN:794,(anonymous_53) +FN:799,(anonymous_54) +FN:804,(anonymous_55) +FN:828,(anonymous_56) +FN:829,(anonymous_57) +FN:831,(anonymous_58) +FN:832,(anonymous_59) +FN:846,(anonymous_60) +FN:852,(anonymous_61) +FN:858,(anonymous_62) +FN:864,(anonymous_63) +FN:936,(anonymous_64) +FN:938,(anonymous_65) +FN:949,(anonymous_66) +FNF:62 +FNH:49 +FNDA:17,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:12,(anonymous_8) +FNDA:10,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:2,(anonymous_11) +FNDA:5,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:2,(anonymous_14) +FNDA:2,(anonymous_15) +FNDA:2,(anonymous_16) +FNDA:2,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:2,(anonymous_19) +FNDA:2,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:6,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:2,(anonymous_31) +FNDA:2,(anonymous_32) +FNDA:2,(anonymous_33) +FNDA:12,(anonymous_34) +FNDA:12,(anonymous_35) +FNDA:12,(anonymous_36) +FNDA:12,(anonymous_37) +FNDA:52,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:2,(anonymous_40) +FNDA:2,(anonymous_41) +FNDA:2,(anonymous_42) +FNDA:2,(anonymous_43) +FNDA:2,(anonymous_44) +FNDA:2,(anonymous_45) +FNDA:2,(anonymous_46) +FNDA:2,(anonymous_47) +FNDA:2,(anonymous_48) +FNDA:2,(anonymous_49) +FNDA:12,(anonymous_50) +FNDA:8,(anonymous_51) +FNDA:16,(anonymous_52) +FNDA:2,(anonymous_53) +FNDA:0,(anonymous_54) +FNDA:2,(anonymous_55) +FNDA:2,(anonymous_56) +FNDA:2,(anonymous_57) +FNDA:2,(anonymous_58) +FNDA:2,(anonymous_59) +FNDA:2,(anonymous_60) +FNDA:2,(anonymous_61) +FNDA:2,(anonymous_62) +FNDA:2,(anonymous_63) +FNDA:6,(anonymous_64) +FNDA:12,(anonymous_65) +FNDA:6,(anonymous_66) +DA:1,17 +DA:12,17 +DA:19,17 +DA:20,17 +DA:24,17 +DA:28,17 +DA:34,17 +DA:39,17 +DA:44,17 +DA:48,17 +DA:54,17 +DA:58,17 +DA:62,17 +DA:70,17 +DA:71,17 +DA:72,17 +DA:73,17 +DA:77,17 +DA:81,17 +DA:85,17 +DA:89,17 +DA:95,17 +DA:96,17 +DA:100,17 +DA:102,17 +DA:104,17 +DA:106,17 +DA:108,17 +DA:110,17 +DA:112,17 +DA:114,17 +DA:116,17 +DA:118,17 +DA:120,17 +DA:122,17 +DA:124,17 +DA:126,17 +DA:128,17 +DA:135,2 +DA:136,1 +DA:144,2 +DA:147,2 +DA:150,2 +DA:151,1 +DA:161,12 +DA:163,12 +DA:164,3 +DA:169,9 +DA:171,7 +DA:183,10 +DA:186,10 +DA:189,10 +DA:191,2 +DA:193,2 +DA:194,0 +DA:198,2 +DA:199,2 +DA:202,2 +DA:204,2 +DA:213,2 +DA:214,2 +DA:215,1 +DA:220,1 +DA:222,1 +DA:233,5 +DA:235,5 +DA:236,5 +DA:237,1 +DA:240,4 +DA:241,4 +DA:242,1 +DA:243,1 +DA:246,3 +DA:247,3 +DA:248,1 +DA:253,2 +DA:254,2 +DA:261,2 +DA:263,2 +DA:264,2 +DA:265,2 +DA:266,2 +DA:267,2 +DA:270,2 +DA:282,2 +DA:283,2 +DA:284,2 +DA:285,2 +DA:290,2 +DA:291,2 +DA:292,2 +DA:293,2 +DA:294,2 +DA:296,2 +DA:297,2 +DA:298,2 +DA:299,2 +DA:300,2 +DA:302,2 +DA:303,2 +DA:304,2 +DA:306,2 +DA:307,0 +DA:310,2 +DA:311,0 +DA:315,2 +DA:319,2 +DA:320,0 +DA:322,0 +DA:323,0 +DA:327,2 +DA:328,0 +DA:329,0 +DA:330,0 +DA:331,0 +DA:336,2 +DA:337,0 +DA:338,0 +DA:339,0 +DA:340,0 +DA:345,2 +DA:349,2 +DA:350,2 +DA:352,2 +DA:353,2 +DA:358,2 +DA:362,2 +DA:363,2 +DA:367,2 +DA:374,2 +DA:376,2 +DA:380,2 +DA:381,2 +DA:382,2 +DA:384,2 +DA:385,0 +DA:390,2 +DA:391,2 +DA:392,2 +DA:393,2 +DA:394,2 +DA:397,2 +DA:398,2 +DA:399,2 +DA:403,2 +DA:404,2 +DA:406,2 +DA:407,2 +DA:408,2 +DA:413,2 +DA:414,2 +DA:417,2 +DA:419,2 +DA:429,2 +DA:430,2 +DA:435,2 +DA:440,2 +DA:441,2 +DA:442,0 +DA:443,2 +DA:444,0 +DA:445,0 +DA:446,2 +DA:447,2 +DA:448,6 +DA:450,2 +DA:451,0 +DA:452,0 +DA:453,2 +DA:454,2 +DA:455,0 +DA:457,2 +DA:458,0 +DA:460,2 +DA:461,0 +DA:464,2 +DA:465,2 +DA:467,2 +DA:468,2 +DA:470,2 +DA:471,2 +DA:474,2 +DA:508,2 +DA:510,2 +DA:514,2 +DA:518,2 +DA:523,2 +DA:527,2 +DA:531,2 +DA:534,2 +DA:535,0 +DA:540,2 +DA:544,2 +DA:556,12 +DA:557,12 +DA:558,12 +DA:559,12 +DA:560,12 +DA:561,12 +DA:564,12 +DA:575,52 +DA:577,52 +DA:578,52 +DA:592,0 +DA:593,0 +DA:596,0 +DA:598,0 +DA:617,2 +DA:619,2 +DA:620,2 +DA:621,2 +DA:622,2 +DA:623,2 +DA:624,2 +DA:629,2 +DA:630,2 +DA:632,2 +DA:633,2 +DA:635,2 +DA:636,2 +DA:639,2 +DA:643,2 +DA:647,2 +DA:652,2 +DA:656,2 +DA:661,2 +DA:684,2 +DA:697,2 +DA:698,2 +DA:700,2 +DA:701,2 +DA:703,2 +DA:704,2 +DA:707,2 +DA:711,2 +DA:715,2 +DA:720,2 +DA:724,2 +DA:728,2 +DA:753,12 +DA:754,0 +DA:756,12 +DA:770,8 +DA:771,0 +DA:774,8 +DA:783,16 +DA:784,16 +DA:798,2 +DA:800,0 +DA:801,0 +DA:802,0 +DA:805,2 +DA:827,2 +DA:828,2 +DA:829,2 +DA:830,2 +DA:831,2 +DA:832,2 +DA:835,2 +DA:837,2 +DA:839,2 +DA:841,2 +DA:843,2 +DA:847,2 +DA:853,2 +DA:859,2 +DA:865,2 +DA:870,2 +DA:872,2 +DA:886,2 +DA:900,2 +DA:914,2 +DA:928,2 +DA:936,17 +DA:937,6 +DA:938,12 +DA:940,6 +DA:941,6 +DA:950,6 +DA:951,6 +DA:952,12 +DA:954,6 +LF:282 +LH:249 +BRDA:144,0,0,0 +BRDA:144,0,1,2 +BRDA:147,1,0,2 +BRDA:147,1,1,2 +BRDA:150,2,0,1 +BRDA:150,2,1,1 +BRDA:163,3,0,3 +BRDA:163,3,1,9 +BRDA:183,4,0,0 +BRDA:183,4,1,10 +BRDA:186,5,0,10 +BRDA:186,5,1,10 +BRDA:189,6,0,8 +BRDA:189,6,1,2 +BRDA:193,7,0,0 +BRDA:193,7,1,2 +BRDA:193,8,0,2 +BRDA:193,8,1,2 +BRDA:202,9,0,0 +BRDA:202,9,1,2 +BRDA:214,10,0,1 +BRDA:214,10,1,1 +BRDA:236,11,0,1 +BRDA:236,11,1,4 +BRDA:241,12,0,1 +BRDA:241,12,1,3 +BRDA:247,13,0,1 +BRDA:247,13,1,2 +BRDA:247,14,0,3 +BRDA:247,14,1,3 +BRDA:265,15,0,2 +BRDA:265,15,1,0 +BRDA:273,16,0,2 +BRDA:273,16,1,0 +BRDA:276,17,0,2 +BRDA:276,17,1,0 +BRDA:303,18,0,2 +BRDA:303,18,1,0 +BRDA:306,19,0,0 +BRDA:306,19,1,2 +BRDA:310,20,0,0 +BRDA:310,20,1,2 +BRDA:322,21,0,0 +BRDA:322,21,1,0 +BRDA:328,22,0,0 +BRDA:328,22,1,0 +BRDA:330,23,0,0 +BRDA:330,23,1,0 +BRDA:337,24,0,0 +BRDA:337,24,1,0 +BRDA:339,25,0,0 +BRDA:339,25,1,0 +BRDA:352,26,0,2 +BRDA:352,26,1,0 +BRDA:363,27,0,2 +BRDA:363,27,1,0 +BRDA:380,28,0,2 +BRDA:380,28,1,0 +BRDA:384,29,0,0 +BRDA:384,29,1,2 +BRDA:385,30,0,0 +BRDA:385,30,1,0 +BRDA:391,31,0,2 +BRDA:391,31,1,0 +BRDA:393,32,0,2 +BRDA:393,32,1,0 +BRDA:398,33,0,2 +BRDA:398,33,1,0 +BRDA:398,34,0,2 +BRDA:398,34,1,2 +BRDA:404,35,0,2 +BRDA:404,35,1,0 +BRDA:407,36,0,2 +BRDA:407,36,1,0 +BRDA:534,37,0,0 +BRDA:534,37,1,2 +BRDA:557,38,0,12 +BRDA:557,38,1,12 +BRDA:558,39,0,12 +BRDA:558,39,1,0 +BRDA:559,40,0,0 +BRDA:559,40,1,12 +BRDA:575,41,0,0 +BRDA:575,41,1,52 +BRDA:575,42,0,52 +BRDA:575,42,1,52 +BRDA:577,43,0,52 +BRDA:577,43,1,0 +BRDA:592,44,0,0 +BRDA:592,44,1,0 +BRDA:598,45,0,0 +BRDA:598,45,1,0 +BRDA:623,46,0,2 +BRDA:623,46,1,0 +BRDA:667,47,0,2 +BRDA:667,47,1,0 +BRDA:668,48,0,2 +BRDA:668,48,1,0 +BRDA:669,49,0,2 +BRDA:669,49,1,0 +BRDA:671,50,0,2 +BRDA:671,50,1,0 +BRDA:673,51,0,2 +BRDA:673,51,1,0 +BRDA:675,52,0,2 +BRDA:675,52,1,0 +BRDA:677,53,0,2 +BRDA:677,53,1,0 +BRDA:679,54,0,2 +BRDA:679,54,1,0 +BRDA:753,55,0,0 +BRDA:753,55,1,12 +BRDA:753,56,0,12 +BRDA:753,56,1,12 +BRDA:770,57,0,0 +BRDA:770,57,1,8 +BRDA:770,58,0,8 +BRDA:770,58,1,8 +BRDA:783,59,0,0 +BRDA:783,59,1,16 +BRDA:800,60,0,0 +BRDA:800,60,1,0 +BRDA:801,61,0,0 +BRDA:801,61,1,0 +BRDA:829,62,0,2 +BRDA:829,62,1,2 +BRDA:832,63,0,2 +BRDA:832,63,1,2 +BRDA:835,64,0,2 +BRDA:835,64,1,0 +BRDA:837,65,0,2 +BRDA:837,65,1,0 +BRDA:839,66,0,2 +BRDA:839,66,1,0 +BRDA:841,67,0,2 +BRDA:841,67,1,0 +BRDA:845,68,0,2 +BRDA:845,68,1,0 +BRDA:847,69,0,2 +BRDA:847,69,1,0 +BRDA:851,70,0,2 +BRDA:851,70,1,0 +BRDA:853,71,0,2 +BRDA:853,71,1,0 +BRDA:857,72,0,2 +BRDA:857,72,1,0 +BRDA:859,73,0,2 +BRDA:859,73,1,0 +BRDA:863,74,0,2 +BRDA:863,74,1,0 +BRDA:865,75,0,2 +BRDA:865,75,1,0 +BRDA:876,76,0,2 +BRDA:876,76,1,0 +BRDA:878,77,0,2 +BRDA:878,77,1,0 +BRDA:890,78,0,2 +BRDA:890,78,1,0 +BRDA:892,79,0,2 +BRDA:892,79,1,0 +BRDA:904,80,0,2 +BRDA:904,80,1,0 +BRDA:906,81,0,2 +BRDA:906,81,1,0 +BRDA:918,82,0,2 +BRDA:918,82,1,0 +BRDA:920,83,0,2 +BRDA:920,83,1,0 +BRDA:938,84,0,12 +BRDA:938,84,1,12 +BRDA:940,85,0,0 +BRDA:940,85,1,6 +BRDA:100,86,0,0 +BRDA:100,86,1,17 +BRDA:100,87,0,17 +BRDA:100,87,1,0 +BRDA:100,88,0,0 +BRDA:100,88,1,17 +BRDA:100,89,0,17 +BRDA:100,89,1,0 +BRDA:102,90,0,0 +BRDA:102,90,1,17 +BRDA:102,91,0,17 +BRDA:102,91,1,0 +BRDA:104,92,0,0 +BRDA:104,92,1,17 +BRDA:104,93,0,17 +BRDA:104,93,1,0 +BRDA:106,94,0,0 +BRDA:106,94,1,17 +BRDA:106,95,0,17 +BRDA:106,95,1,0 +BRDA:108,96,0,0 +BRDA:108,96,1,17 +BRDA:108,97,0,17 +BRDA:108,97,1,0 +BRDA:110,98,0,0 +BRDA:110,98,1,17 +BRDA:110,99,0,17 +BRDA:110,99,1,0 +BRDA:112,100,0,0 +BRDA:112,100,1,17 +BRDA:112,101,0,17 +BRDA:112,101,1,0 +BRDA:114,102,0,0 +BRDA:114,102,1,17 +BRDA:114,103,0,17 +BRDA:114,103,1,0 +BRDA:116,104,0,0 +BRDA:116,104,1,17 +BRDA:116,105,0,17 +BRDA:116,105,1,0 +BRDA:118,106,0,0 +BRDA:118,106,1,17 +BRDA:118,107,0,17 +BRDA:118,107,1,0 +BRDA:120,108,0,0 +BRDA:120,108,1,17 +BRDA:120,109,0,17 +BRDA:120,109,1,0 +BRDA:122,110,0,0 +BRDA:122,110,1,17 +BRDA:122,111,0,17 +BRDA:122,111,1,0 +BRDA:124,112,0,0 +BRDA:124,112,1,17 +BRDA:124,113,0,17 +BRDA:124,113,1,0 +BRDA:126,114,0,0 +BRDA:126,114,1,17 +BRDA:126,115,0,17 +BRDA:126,115,1,0 +BRF:232 +BRH:125 +end_of_record +TN: +SF:src/contexts/reporting/application/services/text-answer.service.ts +FN:20,(anonymous_4) +FN:27,(anonymous_5) +FN:42,(anonymous_6) +FN:48,(anonymous_7) +FN:50,(anonymous_8) +FN:66,(anonymous_9) +FNF:6 +FNH:6 +FNDA:17,(anonymous_4) +FNDA:6,(anonymous_5) +FNDA:5,(anonymous_6) +FNDA:7,(anonymous_7) +FNDA:5,(anonymous_8) +FNDA:6,(anonymous_9) +DA:1,17 +DA:6,17 +DA:8,17 +DA:12,17 +DA:19,17 +DA:22,17 +DA:24,17 +DA:31,6 +DA:33,5 +DA:41,5 +DA:42,5 +DA:43,5 +DA:46,5 +DA:48,7 +DA:51,5 +DA:52,5 +DA:67,6 +DA:70,5 +DA:73,5 +DA:75,1 +LF:20 +LH:20 +BRDA:48,0,0,7 +BRDA:48,0,1,6 +BRDA:54,1,0,5 +BRDA:54,1,1,1 +BRDA:67,2,0,1 +BRDA:67,2,1,5 +BRDA:70,3,0,5 +BRDA:70,3,1,4 +BRDA:73,4,0,4 +BRDA:73,4,1,1 +BRDA:22,5,0,0 +BRDA:22,5,1,17 +BRDA:22,6,0,17 +BRDA:22,6,1,0 +BRDA:22,7,0,0 +BRDA:22,7,1,17 +BRDA:22,8,0,17 +BRDA:22,8,1,0 +BRF:18 +BRH:14 +end_of_record +TN: +SF:src/contexts/reporting/infrastructure/prisma-repositories/report-analytics.repository.ts +FN:14,(anonymous_2) +FN:16,(anonymous_3) +FN:24,(anonymous_4) +FN:31,(anonymous_5) +FN:40,(anonymous_6) +FNF:5 +FNH:5 +FNDA:17,(anonymous_2) +FNDA:10,(anonymous_3) +FNDA:11,(anonymous_4) +FNDA:6,(anonymous_5) +FNDA:4,(anonymous_6) +DA:1,17 +DA:2,17 +DA:3,17 +DA:8,17 +DA:11,17 +DA:12,17 +DA:14,17 +DA:20,10 +DA:21,2 +DA:24,8 +DA:25,11 +DA:28,8 +DA:32,6 +DA:37,6 +DA:41,4 +DA:45,4 +LF:16 +LH:16 +BRDA:20,0,0,2 +BRDA:20,0,1,8 +BRDA:45,1,0,2 +BRDA:45,1,1,2 +BRDA:14,2,0,17 +BRDA:14,2,1,0 +BRDA:14,3,0,17 +BRDA:14,3,1,17 +BRF:8 +BRH:7 +end_of_record +TN: +SF:src/contexts/reporting/infrastructure/prisma-repositories/report-comment.repository.ts +FN:14,(anonymous_2) +FN:16,(anonymous_3) +FN:23,(anonymous_4) +FN:32,(anonymous_5) +FNF:4 +FNH:4 +FNDA:17,(anonymous_2) +FNDA:9,(anonymous_3) +FNDA:4,(anonymous_4) +FNDA:4,(anonymous_5) +DA:1,17 +DA:2,17 +DA:3,17 +DA:8,17 +DA:11,17 +DA:12,17 +DA:14,17 +DA:17,9 +DA:20,9 +DA:24,4 +DA:29,4 +DA:33,4 +DA:37,4 +LF:13 +LH:13 +BRDA:37,0,0,2 +BRDA:37,0,1,2 +BRDA:14,1,0,17 +BRDA:14,1,1,0 +BRDA:14,2,0,17 +BRDA:14,2,1,17 +BRF:6 +BRH:5 +end_of_record +TN: +SF:src/contexts/reporting/infrastructure/prisma-repositories/report-insight.repository.ts +FN:14,(anonymous_2) +FN:16,(anonymous_3) +FN:24,(anonymous_4) +FN:31,(anonymous_5) +FN:40,(anonymous_6) +FNF:5 +FNH:5 +FNDA:17,(anonymous_2) +FNDA:8,(anonymous_3) +FNDA:23,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:4,(anonymous_6) +DA:1,17 +DA:2,17 +DA:3,17 +DA:8,17 +DA:11,17 +DA:12,17 +DA:14,17 +DA:20,8 +DA:21,1 +DA:24,7 +DA:25,23 +DA:28,7 +DA:32,4 +DA:37,4 +DA:41,4 +DA:45,4 +LF:16 +LH:16 +BRDA:20,0,0,1 +BRDA:20,0,1,7 +BRDA:45,1,0,2 +BRDA:45,1,1,2 +BRDA:14,2,0,17 +BRDA:14,2,1,0 +BRDA:14,3,0,17 +BRDA:14,3,1,17 +BRF:8 +BRH:7 +end_of_record +TN: +SF:src/contexts/reporting/infrastructure/prisma-repositories/report.repository.ts +FN:20,(anonymous_2) +FN:22,(anonymous_3) +FN:29,(anonymous_4) +FN:42,(anonymous_5) +FN:55,(anonymous_6) +FN:68,(anonymous_7) +FN:102,(anonymous_8) +FN:137,(anonymous_9) +FNF:8 +FNH:7 +FNDA:17,(anonymous_2) +FNDA:9,(anonymous_3) +FNDA:59,(anonymous_4) +FNDA:20,(anonymous_5) +FNDA:10,(anonymous_6) +FNDA:9,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:9,(anonymous_9) +DA:2,17 +DA:7,17 +DA:8,17 +DA:9,17 +DA:14,17 +DA:17,17 +DA:18,17 +DA:20,17 +DA:23,9 +DA:24,9 +DA:25,9 +DA:26,9 +DA:30,59 +DA:39,59 +DA:43,20 +DA:52,20 +DA:56,10 +DA:65,10 +DA:94,9 +DA:95,9 +DA:103,0 +DA:140,9 +DA:141,9 +DA:142,9 +LF:24 +LH:23 +BRDA:52,0,0,15 +BRDA:52,0,1,5 +BRDA:65,1,0,4 +BRDA:65,1,1,6 +BRDA:96,2,0,0 +BRDA:96,2,1,9 +BRDA:97,3,0,4 +BRDA:97,3,1,5 +BRDA:98,4,0,1 +BRDA:98,4,1,8 +BRDA:99,5,0,0 +BRDA:99,5,1,9 +BRDA:108,6,0,0 +BRDA:108,6,1,9 +BRDA:109,7,0,0 +BRDA:109,7,1,9 +BRDA:110,8,0,0 +BRDA:110,8,1,9 +BRDA:111,9,0,0 +BRDA:111,9,1,9 +BRDA:112,10,0,0 +BRDA:112,10,1,9 +BRDA:113,11,0,0 +BRDA:113,11,1,9 +BRDA:114,12,0,0 +BRDA:114,12,1,9 +BRDA:115,13,0,0 +BRDA:115,13,1,9 +BRDA:116,14,0,0 +BRDA:116,14,1,9 +BRDA:117,15,0,0 +BRDA:117,15,1,9 +BRDA:118,16,0,0 +BRDA:118,16,1,9 +BRDA:121,17,0,0 +BRDA:121,17,1,9 +BRDA:122,18,0,0 +BRDA:122,18,1,9 +BRDA:123,19,0,0 +BRDA:123,19,1,9 +BRDA:124,20,0,0 +BRDA:124,20,1,9 +BRDA:125,21,0,0 +BRDA:125,21,1,9 +BRDA:126,22,0,0 +BRDA:126,22,1,9 +BRDA:127,23,0,0 +BRDA:127,23,1,9 +BRDA:130,24,0,0 +BRDA:130,24,1,9 +BRDA:133,25,0,0 +BRDA:133,25,1,9 +BRDA:140,26,0,9 +BRDA:140,26,1,8 +BRDA:141,27,0,9 +BRDA:141,27,1,8 +BRDA:20,28,0,17 +BRDA:20,28,1,0 +BRDA:20,29,0,17 +BRDA:20,29,1,17 +BRF:60 +BRH:37 +end_of_record +TN: +SF:src/contexts/reporting/infrastructure/prisma-repositories/startegic-report-insight.repository.ts +FN:15,(anonymous_2) +FN:17,(anonymous_3) +FN:25,(anonymous_4) +FN:32,(anonymous_5) +FN:43,(anonymous_6) +FNF:5 +FNH:5 +FNDA:17,(anonymous_2) +FNDA:8,(anonymous_3) +FNDA:15,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:4,(anonymous_6) +DA:1,17 +DA:2,17 +DA:3,17 +DA:8,17 +DA:11,17 +DA:13,17 +DA:15,17 +DA:21,8 +DA:22,1 +DA:25,7 +DA:26,15 +DA:29,7 +DA:35,4 +DA:40,4 +DA:44,4 +DA:48,4 +LF:16 +LH:16 +BRDA:21,0,0,1 +BRDA:21,0,1,7 +BRDA:48,1,0,2 +BRDA:48,1,1,2 +BRDA:15,2,0,17 +BRDA:15,2,1,0 +BRDA:15,3,0,17 +BRDA:15,3,1,17 +BRF:8 +BRH:7 +end_of_record +TN: +SF:src/contexts/reporting/infrastructure/prisma-repositories/strategic-report-analytics.repository.ts +FN:15,(anonymous_2) +FN:17,(anonymous_3) +FN:25,(anonymous_4) +FN:32,(anonymous_5) +FN:43,(anonymous_6) +FNF:5 +FNH:5 +FNDA:17,(anonymous_2) +FNDA:8,(anonymous_3) +FNDA:9,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:4,(anonymous_6) +DA:1,17 +DA:2,17 +DA:3,17 +DA:8,17 +DA:11,17 +DA:13,17 +DA:15,17 +DA:21,8 +DA:22,1 +DA:25,7 +DA:26,9 +DA:29,7 +DA:35,4 +DA:40,4 +DA:44,4 +DA:50,4 +LF:16 +LH:16 +BRDA:21,0,0,1 +BRDA:21,0,1,7 +BRDA:50,1,0,2 +BRDA:50,1,1,2 +BRDA:15,2,0,17 +BRDA:15,2,1,0 +BRDA:15,3,0,17 +BRDA:15,3,1,17 +BRF:8 +BRH:7 +end_of_record +TN: +SF:src/contexts/reporting/infrastructure/prisma-repositories/strategic-report.repository.ts +FN:20,(anonymous_2) +FN:22,(anonymous_3) +FN:34,(anonymous_4) +FN:48,(anonymous_5) +FN:62,(anonymous_6) +FN:79,(anonymous_7) +FN:154,(anonymous_8) +FNF:7 +FNH:7 +FNDA:17,(anonymous_2) +FNDA:5,(anonymous_3) +FNDA:42,(anonymous_4) +FNDA:16,(anonymous_5) +FNDA:11,(anonymous_6) +FNDA:5,(anonymous_7) +FNDA:5,(anonymous_8) +DA:2,17 +DA:7,17 +DA:8,17 +DA:9,17 +DA:14,17 +DA:17,17 +DA:18,17 +DA:20,17 +DA:25,5 +DA:26,5 +DA:27,5 +DA:31,5 +DA:37,42 +DA:45,42 +DA:49,16 +DA:57,16 +DA:65,11 +DA:74,11 +DA:112,5 +DA:113,5 +DA:157,5 +DA:158,5 +DA:159,5 +LF:23 +LH:23 +BRDA:57,0,0,12 +BRDA:57,0,1,4 +BRDA:74,1,0,5 +BRDA:74,1,1,6 +BRDA:114,2,0,1 +BRDA:114,2,1,4 +BRDA:115,3,0,0 +BRDA:115,3,1,5 +BRDA:116,4,0,1 +BRDA:116,4,1,4 +BRDA:117,5,0,0 +BRDA:117,5,1,5 +BRDA:118,6,0,0 +BRDA:118,6,1,5 +BRDA:121,7,0,0 +BRDA:121,7,1,5 +BRDA:122,8,0,0 +BRDA:122,8,1,5 +BRDA:123,9,0,0 +BRDA:123,9,1,5 +BRDA:124,10,0,0 +BRDA:124,10,1,5 +BRDA:125,11,0,0 +BRDA:125,11,1,5 +BRDA:126,12,0,0 +BRDA:126,12,1,5 +BRDA:127,13,0,0 +BRDA:127,13,1,5 +BRDA:128,14,0,0 +BRDA:128,14,1,5 +BRDA:129,15,0,0 +BRDA:129,15,1,5 +BRDA:132,16,0,0 +BRDA:132,16,1,5 +BRDA:133,17,0,0 +BRDA:133,17,1,5 +BRDA:134,18,0,0 +BRDA:134,18,1,5 +BRDA:135,19,0,0 +BRDA:135,19,1,5 +BRDA:136,20,0,0 +BRDA:136,20,1,5 +BRDA:137,21,0,0 +BRDA:137,21,1,5 +BRDA:138,22,0,0 +BRDA:138,22,1,5 +BRDA:139,23,0,0 +BRDA:139,23,1,5 +BRDA:140,24,0,0 +BRDA:140,24,1,5 +BRDA:141,25,0,0 +BRDA:141,25,1,5 +BRDA:142,26,0,0 +BRDA:142,26,1,5 +BRDA:143,27,0,0 +BRDA:143,27,1,5 +BRDA:144,28,0,0 +BRDA:144,28,1,5 +BRDA:145,29,0,0 +BRDA:145,29,1,5 +BRDA:148,30,0,0 +BRDA:148,30,1,5 +BRDA:157,31,0,5 +BRDA:157,31,1,4 +BRDA:158,32,0,5 +BRDA:158,32,1,4 +BRDA:20,33,0,17 +BRDA:20,33,1,0 +BRDA:20,34,0,17 +BRDA:20,34,1,17 +BRF:70 +BRH:42 +end_of_record diff --git a/apps/api/test/integration/dashboard.html b/apps/api/test/integration/dashboard.html new file mode 100644 index 00000000..89e3e62e --- /dev/null +++ b/apps/api/test/integration/dashboard.html @@ -0,0 +1,429 @@ + + + + + + Integration Tests Dashboard + + + + + +
+

+ Integration Tests Dashboard + PASS +

+
+ Integration suite · згенеровано 19 травня 2026 р. о 00:52 +
+
+ +
+
+
Test suites
+
57
+
+ 57 passed +
+
+
+
Tests
+
536
+
+ 536 passed +
+
+
+
Pass rate
+
100.0%
+
536/536 tests
+
+
+
Duration
+
15.32 s
+
cumulative across suites
+
+
+ +
+
+

Tests per context

+
+
+
+

Distribution by context

+
+
+
+ +
+
+

Suites per context

+
+
+
+

Duration per context

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ContextSuitesTestsPassedFailedDuration
+ identity + 450500866 ms
+ organisation + 6727201.73 s
+ library + 9999902.41 s
+ feedback360 + 1816916905.43 s
+ reporting + 1711011003.96 s
+ notifications + 336360921 ms
+ +
+ Згенеровано із Jest AggregatedResult · + 57 spec files +
+ + + + \ No newline at end of file diff --git a/apps/api/test/integration/jest.integration.config.ts b/apps/api/test/integration/jest.integration.config.ts new file mode 100644 index 00000000..b971740d --- /dev/null +++ b/apps/api/test/integration/jest.integration.config.ts @@ -0,0 +1,47 @@ +import type { Config } from 'jest'; + +/** + * Jest configuration for integration tests. + * + * Lives next to the integration test files so it travels with them. + * `rootDir` points back at `apps/api/` so `/src/...` mappings + * keep working as if Jest were invoked from there. Used via + * `pnpm test:integ` / `pnpm test:integ:cov` from `apps/api/`. + */ +const config: Config = { + moduleFileExtensions: ['js', 'json', 'ts'], + // The config file is at apps/api/test/integration/, so two levels up + // lands at apps/api/ — the directory that holds src/. + rootDir: '../..', + moduleNameMapper: { + '^src/(.*)$': '/src/$1', + }, + testRegex: '\\.integration-spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + testEnvironment: 'node', + verbose: false, + // Silence NestJS Logger output (see setup-jest.ts for the reasoning). + // `setupFilesAfterEnv` runs AFTER Jest's test environment is installed, + // so `jest.spyOn` etc. are available inside setup-jest.ts. + setupFilesAfterEnv: ['/test/integration/setup-jest.ts'], + // Integration tests share a single Postgres database, so they must + // run in-band to avoid interleaved TRUNCATEs. + maxWorkers: 1, + testTimeout: 30000, + // Coverage (enabled via `--coverage` / `pnpm test:integ:cov`). + // Scope: application services + infrastructure (repositories, + // mappers) — the layers integration tests actually exercise. The + // unit-test config covers application services from a different + // angle, so the two reports complement each other. + coverageDirectory: 'test/integration/coverage', + collectCoverageFrom: [ + 'src/contexts/*/application/services/**/*.ts', + 'src/contexts/*/application/listeners/**/*.ts', + 'src/contexts/*/infrastructure/prisma-repositories/**/*.ts', + '!src/contexts/*/**/*.spec.ts', + ], +}; + +export default config; diff --git a/apps/api/test/integration/results.json b/apps/api/test/integration/results.json new file mode 100644 index 00000000..bf4f0ecd --- /dev/null +++ b/apps/api/test/integration/results.json @@ -0,0 +1,11618 @@ +{ + "numFailedTestSuites": 0, + "numFailedTests": 0, + "numPassedTestSuites": 57, + "numPassedTests": 536, + "numPendingTestSuites": 0, + "numPendingTests": 0, + "numRuntimeErrorTestSuites": 0, + "numTodoTests": 0, + "numTotalTestSuites": 57, + "numTotalTests": 536, + "openHandles": [], + "snapshot": { + "added": 0, + "didUpdate": false, + "failure": false, + "filesAdded": 0, + "filesRemoved": 0, + "filesRemovedList": [], + "filesUnmatched": 0, + "filesUpdated": 0, + "matched": 0, + "total": 0, + "unchecked": 0, + "uncheckedKeysByFile": [], + "unmatched": 0, + "updated": 0 + }, + "startTime": 1779141105704, + "success": false, + "testResults": [ + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 31, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141107228, + "loadTestEnvironmentEnd": 1779141105844, + "loadTestEnvironmentStart": 1779141105814, + "runtime": 1384, + "setupAfterEnvEnd": 1779141105999, + "setupAfterEnvStart": 1779141105889, + "setupFilesEnd": 1779141105844, + "setupFilesStart": 1779141105844, + "slow": false, + "start": 1779141105844 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/service/review.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewService (integration)", + "create" + ], + "duration": 119, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) create persists a review with NEW stage by default", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141106334, + "status": "passed", + "title": "persists a review with NEW stage by default" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "create" + ], + "duration": 41, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) create throws NotFoundException when cycleId references a missing cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106453, + "status": "passed", + "title": "throws NotFoundException when cycleId references a missing cycle" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "search / getById / RBAC" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) search / getById / RBAC getById throws NotFoundException for a missing review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106494, + "status": "passed", + "title": "getById throws NotFoundException for a missing review" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "search / getById / RBAC" + ], + "duration": 27, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) search / getById / RBAC search without actor returns nothing (no access)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106519, + "status": "passed", + "title": "search without actor returns nothing (no access)" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "search / getById / RBAC" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) search / getById / RBAC search with HR actor returns all reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106546, + "status": "passed", + "title": "search with HR actor returns all reviews" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "search / getById / RBAC" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) search / getById / RBAC getById with ratee actor succeeds", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106571, + "status": "passed", + "title": "getById with ratee actor succeeds" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "search / getById / RBAC" + ], + "duration": 27, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) search / getById / RBAC getById with an unrelated actor throws ForbiddenException", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106594, + "status": "passed", + "title": "getById with an unrelated actor throws ForbiddenException" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "update" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) update persists patched fields when the review is NEW", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106622, + "status": "passed", + "title": "persists patched fields when the review is NEW" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "update" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) update rejects updates when the review has progressed past PROCESSING_BY_HR", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106645, + "status": "passed", + "title": "rejects updates when the review has progressed past PROCESSING_BY_HR" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "delete" + ], + "duration": 30, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) delete removes the review row and its dependent rows", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106669, + "status": "passed", + "title": "removes the review row and its dependent rows" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "delete" + ], + "duration": 27, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) delete rejects deletion when answers are already submitted", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106699, + "status": "passed", + "title": "rejects deletion when answers are already submitted" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "createQuestion / deleteQuestion" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) createQuestion / deleteQuestion persists a question without requiring a template", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141106726, + "status": "passed", + "title": "persists a question without requiring a template" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "createQuestion / deleteQuestion" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) createQuestion / deleteQuestion inherits title / answerType from the template when not specified", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141106747, + "status": "passed", + "title": "inherits title / answerType from the template when not specified" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "createQuestion / deleteQuestion" + ], + "duration": 27, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) createQuestion / deleteQuestion deleteQuestion removes the row when the cycle is still active", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106768, + "status": "passed", + "title": "deleteQuestion removes the row when the cycle is still active" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "createQuestion / deleteQuestion" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) createQuestion / deleteQuestion deleteQuestion rejects when the parent cycle is finished", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106795, + "status": "passed", + "title": "deleteQuestion rejects when the parent cycle is finished" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "attachQuestion / detachQuestion / listQuestionRelations" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) attachQuestion / detachQuestion / listQuestionRelations attaches a question template to a NEW review and creates the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141106818, + "status": "passed", + "title": "attaches a question template to a NEW review and creates the relation" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "attachQuestion / detachQuestion / listQuestionRelations" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) attachQuestion / detachQuestion / listQuestionRelations rejects attaching a question once the review has started", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106847, + "status": "passed", + "title": "rejects attaching a question once the review has started" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "attachQuestion / detachQuestion / listQuestionRelations" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) attachQuestion / detachQuestion / listQuestionRelations detachQuestion removes the relation while the review is NEW", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106867, + "status": "passed", + "title": "detachQuestion removes the relation while the review is NEW" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addRespondent / changeRespondentStatus / completeReview" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addRespondent / changeRespondentStatus / completeReview persists a respondent and the row appears in listRespondents", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141106896, + "status": "passed", + "title": "persists a respondent and the row appears in listRespondents" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addRespondent / changeRespondentStatus / completeReview" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addRespondent / changeRespondentStatus / completeReview rejects adding a respondent when the review is finished", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106925, + "status": "passed", + "title": "rejects adding a respondent when the review is finished" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addRespondent / changeRespondentStatus / completeReview" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addRespondent / changeRespondentStatus / completeReview changeRespondentStatus validates the transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106948, + "status": "passed", + "title": "changeRespondentStatus validates the transition" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addRespondent / changeRespondentStatus / completeReview" + ], + "duration": 37, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addRespondent / changeRespondentStatus / completeReview completeReview transitions IN_PROGRESS → FINISHED when all respondents are COMPLETED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141106970, + "status": "passed", + "title": "completeReview transitions IN_PROGRESS → FINISHED when all respondents are COMPLETED" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addRespondent / changeRespondentStatus / completeReview" + ], + "duration": 30, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addRespondent / changeRespondentStatus / completeReview completeReview rejects when at least one respondent is still PENDING", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107008, + "status": "passed", + "title": "completeReview rejects when at least one respondent is still PENDING" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addReviewer / removeReviewer" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addReviewer / removeReviewer persists a reviewer row attached to the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141107038, + "status": "passed", + "title": "persists a reviewer row attached to the review" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addReviewer / removeReviewer" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addReviewer / removeReviewer removeReviewer drops the row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107060, + "status": "passed", + "title": "removeReviewer drops the row" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addAnswer" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addAnswer persists an answer when the review is IN_PROGRESS", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141107090, + "status": "passed", + "title": "persists an answer when the review is IN_PROGRESS" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "addAnswer" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) addAnswer rejects answers when the review is still NEW", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107112, + "status": "passed", + "title": "rejects answers when the review is still NEW" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "upsertClusterScore / listClusterScores" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) upsertClusterScore / listClusterScores creates and then updates a cluster score for the same (cluster, ratee) pair", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141107133, + "status": "passed", + "title": "creates and then updates a cluster score for the same (cluster, ratee) pair" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "changeReviewStage / getStageHistory / forceFinishReview" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) changeReviewStage / getStageHistory / forceFinishReview rejects an invalid transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107158, + "status": "passed", + "title": "rejects an invalid transition" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "changeReviewStage / getStageHistory / forceFinishReview" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) changeReviewStage / getStageHistory / forceFinishReview valid transition persists the new stage and writes a history record", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141107181, + "status": "passed", + "title": "valid transition persists the new stage and writes a history record" + }, + { + "ancestorTitles": [ + "ReviewService (integration)", + "changeReviewStage / getStageHistory / forceFinishReview" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService (integration) changeReviewStage / getStageHistory / forceFinishReview forceFinishReview moves an IN_PROGRESS review to FINISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107204, + "status": "passed", + "title": "forceFinishReview moves an IN_PROGRESS review to FINISHED" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 21, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141107601, + "loadTestEnvironmentEnd": 1779141107233, + "loadTestEnvironmentStart": 1779141107230, + "runtime": 368, + "setupAfterEnvEnd": 1779141107257, + "setupAfterEnvStart": 1779141107241, + "setupFilesEnd": 1779141107233, + "setupFilesStart": 1779141107233, + "slow": false, + "start": 1779141107233 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/identity/service/identity-user.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "create" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) create persists a new user with default status and no roles", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141107315, + "status": "passed", + "title": "persists a new user with default status and no roles" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "create" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) create persists a user together with the requested roles", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107334, + "status": "passed", + "title": "persists a user together with the requested roles" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "create" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) create emits a user.created event with the persisted id and email", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141107350, + "status": "passed", + "title": "emits a user.created event with the persisted id and email" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "create" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) create rejects a duplicate email with BadRequestException", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107362, + "status": "passed", + "title": "rejects a duplicate email with BadRequestException" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "create" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) create honours a supplied fullName instead of rebuilding it", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107376, + "status": "passed", + "title": "honours a supplied fullName instead of rebuilding it" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "getById / findByEmail / search" + ], + "duration": 11, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) getById / findByEmail / search returns NotFoundException when the user does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107388, + "status": "passed", + "title": "returns NotFoundException when the user does not exist" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "getById / findByEmail / search" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) getById / findByEmail / search returns the user without roles when withRoles is not requested", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141107399, + "status": "passed", + "title": "returns the user without roles when withRoles is not requested" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "getById / findByEmail / search" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) getById / findByEmail / search returns the user with roles when withRoles is requested", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107411, + "status": "passed", + "title": "returns the user with roles when withRoles is requested" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "getById / findByEmail / search" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) getById / findByEmail / search findByEmail returns the user or null", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141107424, + "status": "passed", + "title": "findByEmail returns the user or null" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "getById / findByEmail / search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) getById / findByEmail / search search filters by status and matches via the search field", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141107436, + "status": "passed", + "title": "search filters by status and matches via the search field" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "update" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) update rebuilds fullName when any name part is patched", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141107452, + "status": "passed", + "title": "rebuilds fullName when any name part is patched" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "update" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) update preserves fullName when no name field is patched", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107466, + "status": "passed", + "title": "preserves fullName when no name field is patched" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "update" + ], + "duration": 11, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) update throws NotFoundException when updating a missing user", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107480, + "status": "passed", + "title": "throws NotFoundException when updating a missing user" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "replaceRoles" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) replaceRoles replaces the user role set, removing roles no longer present", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141107491, + "status": "passed", + "title": "replaces the user role set, removing roles no longer present" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "replaceRoles" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) replaceRoles clears all roles when given an empty list", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141107507, + "status": "passed", + "title": "clears all roles when given an empty list" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "replaceRoles" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) replaceRoles deduplicates the requested role list", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107523, + "status": "passed", + "title": "deduplicates the requested role list" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "delete" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) delete removes the user row from the database", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107538, + "status": "passed", + "title": "removes the user row from the database" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "delete" + ], + "duration": 10, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) delete throws NotFoundException when deleting a missing user", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107553, + "status": "passed", + "title": "throws NotFoundException when deleting a missing user" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "upsertExternalUser" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) upsertExternalUser creates a new EMPLOYEE-role user when none exists for the email", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141107563, + "status": "passed", + "title": "creates a new EMPLOYEE-role user when none exists for the email" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)", + "upsertExternalUser" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) upsertExternalUser returns the existing user when one with the email is already stored", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141107575, + "status": "passed", + "title": "returns the existing user when one with the email is already stored" + }, + { + "ancestorTitles": [ + "IdentityUserService (integration)" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService (integration) persisted user matches the domain object returned by create", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141107587, + "status": "passed", + "title": "persisted user matches the domain object returned by create" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 8, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141107804, + "loadTestEnvironmentEnd": 1779141107603, + "loadTestEnvironmentStart": 1779141107602, + "runtime": 201, + "setupAfterEnvEnd": 1779141107624, + "setupAfterEnvStart": 1779141107611, + "setupFilesEnd": 1779141107603, + "setupFilesStart": 1779141107603, + "slow": false, + "start": 1779141107603 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/repository/competence.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceRepository (integration)", + "create" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceRepository (integration) create persists a competence and round-trips through Prisma", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141107669, + "status": "passed", + "title": "persists a competence and round-trips through Prisma" + }, + { + "ancestorTitles": [ + "CompetenceRepository (integration)", + "findById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceRepository (integration) findById returns the competence when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107688, + "status": "passed", + "title": "returns the competence when found" + }, + { + "ancestorTitles": [ + "CompetenceRepository (integration)", + "findById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceRepository (integration) findById returns null when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107702, + "status": "passed", + "title": "returns null when missing" + }, + { + "ancestorTitles": [ + "CompetenceRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceRepository (integration) search returns all when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107715, + "status": "passed", + "title": "returns all when no filter is supplied" + }, + { + "ancestorTitles": [ + "CompetenceRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceRepository (integration) search filters by code (case insensitive substring)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107729, + "status": "passed", + "title": "filters by code (case insensitive substring)" + }, + { + "ancestorTitles": [ + "CompetenceRepository (integration)", + "search" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceRepository (integration) search honours descending sort on title", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107743, + "status": "passed", + "title": "honours descending sort on title" + }, + { + "ancestorTitles": [ + "CompetenceRepository (integration)", + "updateById" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceRepository (integration) updateById persists the patched fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107766, + "status": "passed", + "title": "persists the patched fields" + }, + { + "ancestorTitles": [ + "CompetenceRepository (integration)", + "deleteById" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceRepository (integration) deleteById removes the competence row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107788, + "status": "passed", + "title": "removes the competence row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 21, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141108233, + "loadTestEnvironmentEnd": 1779141107807, + "loadTestEnvironmentStart": 1779141107804, + "runtime": 426, + "setupAfterEnvEnd": 1779141107826, + "setupAfterEnvStart": 1779141107814, + "setupFilesEnd": 1779141107807, + "setupFilesStart": 1779141107807, + "slow": false, + "start": 1779141107807 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/service/competence.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceService (integration)", + "create" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) create persists a new competence with the supplied fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141107865, + "status": "passed", + "title": "persists a new competence with the supplied fields" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "create" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) create coerces missing code/description to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141107888, + "status": "passed", + "title": "coerces missing code/description to null" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "search" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) search returns all competences when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107902, + "status": "passed", + "title": "returns all competences when no filter is supplied" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "search" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) search filters by title substring (case insensitive)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107917, + "status": "passed", + "title": "filters by title substring (case insensitive)" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "search" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) search filters by free-text \"search\" across title/code/description", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107931, + "status": "passed", + "title": "filters by free-text \"search\" across title/code/description" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "getById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) getById returns the competence when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107944, + "status": "passed", + "title": "returns the competence when found" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "getById" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) getById throws NotFoundException for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107957, + "status": "passed", + "title": "throws NotFoundException for a missing id" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "update" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) update persists the patched fields and ignores undefined keys", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141107969, + "status": "passed", + "title": "persists the patched fields and ignores undefined keys" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "update" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) update throws NotFoundException when the competence is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107983, + "status": "passed", + "title": "throws NotFoundException when the competence is missing" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "delete" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) delete removes the competence when nothing is linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141107995, + "status": "passed", + "title": "removes the competence when nothing is linked" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "delete" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) delete throws NotFoundException for a missing competence", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108012, + "status": "passed", + "title": "throws NotFoundException for a missing competence" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "delete" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) delete rejects deletion when at least one position is linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108024, + "status": "passed", + "title": "rejects deletion when at least one position is linked" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "delete" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) delete rejects deletion when at least one question template is linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108039, + "status": "passed", + "title": "rejects deletion when at least one question template is linked" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "attachPosition / detachPosition / listPositions" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) attachPosition / detachPosition / listPositions links a competence to a position and returns the new list", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108054, + "status": "passed", + "title": "links a competence to a position and returns the new list" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "attachPosition / detachPosition / listPositions" + ], + "duration": 72, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) attachPosition / detachPosition / listPositions is idempotent: attaching the same position twice keeps a single row", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108071, + "status": "passed", + "title": "is idempotent: attaching the same position twice keeps a single row" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "attachPosition / detachPosition / listPositions" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) attachPosition / detachPosition / listPositions throws NotFoundException when the position is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108143, + "status": "passed", + "title": "throws NotFoundException when the position is missing" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "attachPosition / detachPosition / listPositions" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) attachPosition / detachPosition / listPositions detachPosition removes the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108161, + "status": "passed", + "title": "detachPosition removes the relation" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "attachPosition / detachPosition / listPositions" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) attachPosition / detachPosition / listPositions detachPosition is a no-op when no relation exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108176, + "status": "passed", + "title": "detachPosition is a no-op when no relation exists" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates links and returns the templates attached to the competence", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108189, + "status": "passed", + "title": "links and returns the templates attached to the competence" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates throws NotFoundException when the question template is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108205, + "status": "passed", + "title": "throws NotFoundException when the question template is missing" + }, + { + "ancestorTitles": [ + "CompetenceService (integration)", + "attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService (integration) attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates detachQuestionTemplate removes the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108217, + "status": "passed", + "title": "detachQuestionTemplate removes the relation" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141108487, + "loadTestEnvironmentEnd": 1779141108235, + "loadTestEnvironmentStart": 1779141108234, + "runtime": 252, + "setupAfterEnvEnd": 1779141108257, + "setupAfterEnvStart": 1779141108243, + "setupFilesEnd": 1779141108235, + "setupFilesStart": 1779141108235, + "slow": false, + "start": 1779141108235 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/notifications/listeners/notification-listeners.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "Notification listeners (integration)", + "CycleStageNotificationListener" + ], + "duration": 26, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) CycleStageNotificationListener dispatches strategic-report-ready email when cycle is PUBLISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141108312, + "status": "passed", + "title": "dispatches strategic-report-ready email when cycle is PUBLISHED" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "CycleStageNotificationListener" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) CycleStageNotificationListener does nothing for non-PUBLISHED transitions", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108338, + "status": "passed", + "title": "does nothing for non-PUBLISHED transitions" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "CycleStageNotificationListener" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) CycleStageNotificationListener swallows errors from the dispatch and does not rethrow", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108353, + "status": "passed", + "title": "swallows errors from the dispatch and does not rethrow" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "ReviewStageNotificationListener" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) ReviewStageNotificationListener emails the ratee when review transitions to SELF_ASSESSMENT", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108375, + "status": "passed", + "title": "emails the ratee when review transitions to SELF_ASSESSMENT" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "ReviewStageNotificationListener" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) ReviewStageNotificationListener emails respondents when review transitions to IN_PROGRESS", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108394, + "status": "passed", + "title": "emails respondents when review transitions to IN_PROGRESS" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "ReviewStageNotificationListener" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) ReviewStageNotificationListener emails HR when review transitions to PROCESSING_BY_HR", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108409, + "status": "passed", + "title": "emails HR when review transitions to PROCESSING_BY_HR" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "ReviewStageNotificationListener" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) ReviewStageNotificationListener emails reviewers when review transitions to PUBLISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108426, + "status": "passed", + "title": "emails reviewers when review transitions to PUBLISHED" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "ReviewStageNotificationListener" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) ReviewStageNotificationListener does nothing for transitions not in the switch", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108441, + "status": "passed", + "title": "does nothing for transitions not in the switch" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "UserCreatedNotificationListener" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) UserCreatedNotificationListener dispatches a welcome email when a user-created event fires", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141108455, + "status": "passed", + "title": "dispatches a welcome email when a user-created event fires" + }, + { + "ancestorTitles": [ + "Notification listeners (integration)", + "UserCreatedNotificationListener" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "Notification listeners (integration) UserCreatedNotificationListener logs and swallows errors from the dispatch", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108471, + "status": "passed", + "title": "logs and swallows errors from the dispatch" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 16, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141108904, + "loadTestEnvironmentEnd": 1779141108489, + "loadTestEnvironmentStart": 1779141108487, + "runtime": 415, + "setupAfterEnvEnd": 1779141108514, + "setupAfterEnvStart": 1779141108497, + "setupFilesEnd": 1779141108489, + "setupFilesStart": 1779141108489, + "slow": false, + "start": 1779141108489 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/notifications/repository/notification-log.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "create" + ], + "duration": 32, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) create persists a log with channel EMAIL by default", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141108560, + "status": "passed", + "title": "persists a log with channel EMAIL by default" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "create" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) create persists a log tied to a review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108592, + "status": "passed", + "title": "persists a log tied to a review" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "create" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) create persists a log tied to a cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108612, + "status": "passed", + "title": "persists a log tied to a cycle" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForReview" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForReview returns the log matching (reviewId, kind, recipientUserId)", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108629, + "status": "passed", + "title": "returns the log matching (reviewId, kind, recipientUserId)" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForReview" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForReview returns null when no match exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108648, + "status": "passed", + "title": "returns null when no match exists" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForReview" + ], + "duration": 59, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForReview enforces the unique (reviewId, kind, recipientUserId) constraint", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108664, + "status": "passed", + "title": "enforces the unique (reviewId, kind, recipientUserId) constraint" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForCycle" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForCycle returns the log matching (cycleId, kind, recipientUserId)", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108723, + "status": "passed", + "title": "returns the log matching (cycleId, kind, recipientUserId)" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForCycle" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForCycle returns null when no match exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108744, + "status": "passed", + "title": "returns null when no match exists" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForCycle" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForCycle enforces the unique (cycleId, kind, recipientUserId) constraint", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108758, + "status": "passed", + "title": "enforces the unique (cycleId, kind, recipientUserId) constraint" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForUser" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForUser returns the welcome-style log for the user (cycleId/reviewId both null)", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141108774, + "status": "passed", + "title": "returns the welcome-style log for the user (cycleId/reviewId both null)" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForUser" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForUser does not match logs that have a reviewId or cycleId set", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108799, + "status": "passed", + "title": "does not match logs that have a reviewId or cycleId set" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "findOneForUser" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) findOneForUser returns null when there is no welcome log", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108823, + "status": "passed", + "title": "returns null when there is no welcome log" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "markSent" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) markSent sets sentAt to a non-null value and clears errorMessage", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108838, + "status": "passed", + "title": "sets sentAt to a non-null value and clears errorMessage" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "markFailure" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) markFailure persists the error message", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108856, + "status": "passed", + "title": "persists the error message" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "cascade behaviour" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) cascade behaviour cascade-deletes logs when their cycle is removed", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141108871, + "status": "passed", + "title": "cascade-deletes logs when their cycle is removed" + }, + { + "ancestorTitles": [ + "NotificationLogRepository (integration)", + "isSent (domain)" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogRepository (integration) isSent (domain) flips from false to true once persisted with sentAt", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141108887, + "status": "passed", + "title": "flips from false to true once persisted with sentAt" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 20, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141109322, + "loadTestEnvironmentEnd": 1779141108906, + "loadTestEnvironmentStart": 1779141108904, + "runtime": 416, + "setupAfterEnvEnd": 1779141108943, + "setupAfterEnvStart": 1779141108914, + "setupFilesEnd": 1779141108906, + "setupFilesStart": 1779141108906, + "slow": false, + "start": 1779141108906 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/organisation/service/team.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamService (integration)", + "create" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) create persists a team without a head", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141108990, + "status": "passed", + "title": "persists a team without a head" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "create" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) create verifies the head user exists when headId is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109015, + "status": "passed", + "title": "verifies the head user exists when headId is supplied" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "create" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) create throws NotFoundException for a missing head user", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109032, + "status": "passed", + "title": "throws NotFoundException for a missing head user" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "search" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) search filters teams by title (case insensitive substring)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109048, + "status": "passed", + "title": "filters teams by title (case insensitive substring)" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "search" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) search returns all teams without filter", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109065, + "status": "passed", + "title": "returns all teams without filter" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "getById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) getById returns the team when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109082, + "status": "passed", + "title": "returns the team when found" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "getById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) getById throws NotFoundException when the team does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109097, + "status": "passed", + "title": "throws NotFoundException when the team does not exist" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "update" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) update forwards the patch and persists the changes", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141109111, + "status": "passed", + "title": "forwards the patch and persists the changes" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "update" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) update throws NotFoundException for a missing team", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109127, + "status": "passed", + "title": "throws NotFoundException for a missing team" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "update" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) update throws NotFoundException for a missing head user", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109141, + "status": "passed", + "title": "throws NotFoundException for a missing head user" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "delete" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) delete removes a team that has no members", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109155, + "status": "passed", + "title": "removes a team that has no members" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "delete" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) delete rejects deleting a team that still has members", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141109172, + "status": "passed", + "title": "rejects deleting a team that still has members" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "delete" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) delete throws NotFoundException when the team does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109191, + "status": "passed", + "title": "throws NotFoundException when the team does not exist" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "addMember / listMembers / removeMember" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) addMember / listMembers / removeMember adds a member without user data by default", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141109206, + "status": "passed", + "title": "adds a member without user data by default" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "addMember / listMembers / removeMember" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) addMember / listMembers / removeMember attaches the user when withUser is set", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141109225, + "status": "passed", + "title": "attaches the user when withUser is set" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "addMember / listMembers / removeMember" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) addMember / listMembers / removeMember rejects adding a missing user", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109241, + "status": "passed", + "title": "rejects adding a missing user" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "addMember / listMembers / removeMember" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) addMember / listMembers / removeMember rejects adding a member to a missing team", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109256, + "status": "passed", + "title": "rejects adding a member to a missing team" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "addMember / listMembers / removeMember" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) addMember / listMembers / removeMember listMembers returns memberships without users by default", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141109270, + "status": "passed", + "title": "listMembers returns memberships without users by default" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "addMember / listMembers / removeMember" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) addMember / listMembers / removeMember listMembers attaches users when withUsers is set", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109287, + "status": "passed", + "title": "listMembers attaches users when withUsers is set" + }, + { + "ancestorTitles": [ + "TeamService (integration)", + "addMember / listMembers / removeMember" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService (integration) addMember / listMembers / removeMember removeMember deletes the membership row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109305, + "status": "passed", + "title": "removeMember deletes the membership row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 16, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141109729, + "loadTestEnvironmentEnd": 1779141109324, + "loadTestEnvironmentStart": 1779141109322, + "runtime": 405, + "setupAfterEnvEnd": 1779141109352, + "setupAfterEnvStart": 1779141109334, + "setupFilesEnd": 1779141109324, + "setupFilesStart": 1779141109324, + "slow": false, + "start": 1779141109324 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/service/cluster-score-analytics.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "upsert" + ], + "duration": 32, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) upsert creates an analytics row when none exists for the (cycle, cluster)", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141109404, + "status": "passed", + "title": "creates an analytics row when none exists for the (cycle, cluster)" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "upsert" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) upsert updates the existing row on the unique (cycleId, clusterId) pair", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141109436, + "status": "passed", + "title": "updates the existing row on the unique (cycleId, clusterId) pair" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "upsert" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) upsert throws NotFoundException when the cycle does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109458, + "status": "passed", + "title": "throws NotFoundException when the cycle does not exist" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "upsert" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) upsert rejects bounds inverted (lower > upper)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109477, + "status": "passed", + "title": "rejects bounds inverted (lower > upper)" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "upsert" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) upsert rejects scores where min > max", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109494, + "status": "passed", + "title": "rejects scores where min > max" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "upsert" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) upsert rejects scores where avg lies outside [min, max]", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109511, + "status": "passed", + "title": "rejects scores where avg lies outside [min, max]" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "getById / search / getByCycleId" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) getById / search / getByCycleId getById returns the row when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109529, + "status": "passed", + "title": "getById returns the row when found" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "getById / search / getByCycleId" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) getById / search / getByCycleId getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109547, + "status": "passed", + "title": "getById throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "getById / search / getByCycleId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) getById / search / getByCycleId search filters by cycleId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109564, + "status": "passed", + "title": "search filters by cycleId" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "getById / search / getByCycleId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) getById / search / getByCycleId getByCycleId returns analytics tied to a cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109583, + "status": "passed", + "title": "getByCycleId returns analytics tied to a cycle" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "update" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) update persists patched fields and re-runs score validation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109602, + "status": "passed", + "title": "persists patched fields and re-runs score validation" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "update" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) update rejects a patch that breaks the min/avg/max invariant", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109621, + "status": "passed", + "title": "rejects a patch that breaks the min/avg/max invariant" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "delete" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) delete removes the analytics row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109640, + "status": "passed", + "title": "removes the analytics row" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "generateAnalyticsForCycle" + ], + "duration": 28, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) generateAnalyticsForCycle aggregates cluster scores into analytics for every cluster with data", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141109659, + "status": "passed", + "title": "aggregates cluster scores into analytics for every cluster with data" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "generateAnalyticsForCycle" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) generateAnalyticsForCycle is a no-op when the cycle has no cluster scores", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109687, + "status": "passed", + "title": "is a no-op when the cycle has no cluster scores" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService (integration)", + "generateAnalyticsForCycle" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService (integration) generateAnalyticsForCycle throws NotFoundException when the cycle does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109710, + "status": "passed", + "title": "throws NotFoundException when the cycle does not exist" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 16, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141110118, + "loadTestEnvironmentEnd": 1779141109731, + "loadTestEnvironmentStart": 1779141109730, + "runtime": 387, + "setupAfterEnvEnd": 1779141109757, + "setupAfterEnvStart": 1779141109742, + "setupFilesEnd": 1779141109731, + "setupFilesStart": 1779141109731, + "slow": false, + "start": 1779141109731 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/service/question-template.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "create" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) create persists a template with defaults applied", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141109804, + "status": "passed", + "title": "persists a template with defaults applied" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "create" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) create throws NotFoundException when the competence is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109828, + "status": "passed", + "title": "throws NotFoundException when the competence is missing" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "create" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) create attaches the supplied positions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141109842, + "status": "passed", + "title": "attaches the supplied positions" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "create" + ], + "duration": 58, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) create rejects creation when one of the supplied positions does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109863, + "status": "passed", + "title": "rejects creation when one of the supplied positions does not exist" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "search / getById" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) search / getById search filters by competenceId", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141109921, + "status": "passed", + "title": "search filters by competenceId" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "search / getById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) search / getById getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109941, + "status": "passed", + "title": "getById throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "update" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) update persists patched scalar fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141109954, + "status": "passed", + "title": "persists patched scalar fields" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "update" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) update validates the new competenceId when changed", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109971, + "status": "passed", + "title": "validates the new competenceId when changed" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "delete" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) delete removes the template and its relations when nothing references it", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141109986, + "status": "passed", + "title": "removes the template and its relations when nothing references it" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "delete" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) delete rejects deletion when a review question references the template", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141110002, + "status": "passed", + "title": "rejects deletion when a review question references the template" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "attachPosition / detachPosition / listPositions" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) attachPosition / detachPosition / listPositions links a template to a position and lists it back", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110018, + "status": "passed", + "title": "links a template to a position and lists it back" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "attachPosition / detachPosition / listPositions" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) attachPosition / detachPosition / listPositions throws NotFoundException when the position does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110035, + "status": "passed", + "title": "throws NotFoundException when the position does not exist" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "attachPosition / detachPosition / listPositions" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) attachPosition / detachPosition / listPositions detachPosition removes the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110049, + "status": "passed", + "title": "detachPosition removes the relation" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "attachCompetence / detachCompetence / listCompetences" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) attachCompetence / detachCompetence / listCompetences links a template to a competence and lists it back", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110066, + "status": "passed", + "title": "links a template to a competence and lists it back" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "attachCompetence / detachCompetence / listCompetences" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) attachCompetence / detachCompetence / listCompetences throws NotFoundException when the competence does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110082, + "status": "passed", + "title": "throws NotFoundException when the competence does not exist" + }, + { + "ancestorTitles": [ + "QuestionTemplateService (integration)", + "attachCompetence / detachCompetence / listCompetences" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService (integration) attachCompetence / detachCompetence / listCompetences detachCompetence removes the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110100, + "status": "passed", + "title": "detachCompetence removes the relation" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 13, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141110477, + "loadTestEnvironmentEnd": 1779141110120, + "loadTestEnvironmentStart": 1779141110119, + "runtime": 357, + "setupAfterEnvEnd": 1779141110144, + "setupAfterEnvStart": 1779141110129, + "setupFilesEnd": 1779141110120, + "setupFilesStart": 1779141110120, + "slow": false, + "start": 1779141110120 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/organisation/repository/team.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamRepository (integration)", + "create" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) create persists a team without a head", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141110190, + "status": "passed", + "title": "persists a team without a head" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "create" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) create persists a team with a head pointing to a real user", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110214, + "status": "passed", + "title": "persists a team with a head pointing to a real user" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "findById / search" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) findById / search findById returns the team when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110231, + "status": "passed", + "title": "findById returns the team when found" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "findById / search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) findById / search findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110248, + "status": "passed", + "title": "findById returns null for a missing id" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "findById / search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) findById / search search filters by headId", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141110265, + "status": "passed", + "title": "search filters by headId" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "findById / search" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) findById / search search supports descending sort on title", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110281, + "status": "passed", + "title": "search supports descending sort on title" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "updateById" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) updateById persists the patched fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110296, + "status": "passed", + "title": "persists the patched fields" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "deleteById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) deleteById removes the team row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110312, + "status": "passed", + "title": "removes the team row" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "addMember / removeMember / listMembers" + ], + "duration": 26, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) addMember / removeMember / listMembers addMember creates the membership row", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141110328, + "status": "passed", + "title": "addMember creates the membership row" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "addMember / removeMember / listMembers" + ], + "duration": 71, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) addMember / removeMember / listMembers addMember is idempotent (returns existing on unique conflict)", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141110354, + "status": "passed", + "title": "addMember is idempotent (returns existing on unique conflict)" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "addMember / removeMember / listMembers" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) addMember / removeMember / listMembers coerces a missing isPrimary to false", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110425, + "status": "passed", + "title": "coerces a missing isPrimary to false" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "addMember / removeMember / listMembers" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) addMember / removeMember / listMembers listMembers returns all memberships for the team", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141110443, + "status": "passed", + "title": "listMembers returns all memberships for the team" + }, + { + "ancestorTitles": [ + "TeamRepository (integration)", + "addMember / removeMember / listMembers" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamRepository (integration) addMember / removeMember / listMembers removeMember deletes the membership row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110460, + "status": "passed", + "title": "removeMember deletes the membership row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 15, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141110843, + "loadTestEnvironmentEnd": 1779141110479, + "loadTestEnvironmentStart": 1779141110478, + "runtime": 364, + "setupAfterEnvEnd": 1779141110498, + "setupAfterEnvStart": 1779141110487, + "setupFilesEnd": 1779141110479, + "setupFilesStart": 1779141110479, + "slow": false, + "start": 1779141110479 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/service/cycle.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleService (integration)", + "create" + ], + "duration": 37, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) create persists a cycle with stage NEW by default", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141110551, + "status": "passed", + "title": "persists a cycle with stage NEW by default" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "search / getById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) search / getById search returns persisted cycles", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110588, + "status": "passed", + "title": "search returns persisted cycles" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "search / getById" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) search / getById getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110607, + "status": "passed", + "title": "getById throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "update" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) update persists patched scalar fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110622, + "status": "passed", + "title": "persists patched scalar fields" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "update" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) update throws NotFoundException when the cycle is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110640, + "status": "passed", + "title": "throws NotFoundException when the cycle is missing" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "update" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) update transitions the stage via changeStage and writes history", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141110655, + "status": "passed", + "title": "transitions the stage via changeStage and writes history" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "update" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) update rejects finishing a cycle while reviews are still in progress", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110679, + "status": "passed", + "title": "rejects finishing a cycle while reviews are still in progress" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "changeStage" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) changeStage rejects an invalid transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110699, + "status": "passed", + "title": "rejects an invalid transition" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "changeStage" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) changeStage persists the stage and emits a history record on a valid transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141110715, + "status": "passed", + "title": "persists the stage and emits a history record on a valid transition" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "completeCycle" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) completeCycle transitions an ACTIVE cycle to FINISHED when all reviews are complete", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110733, + "status": "passed", + "title": "transitions an ACTIVE cycle to FINISHED when all reviews are complete" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "completeCycle" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) completeCycle throws BadRequestException when reviews are still in progress", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110754, + "status": "passed", + "title": "throws BadRequestException when reviews are still in progress" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "completeCycle" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) completeCycle is a no-op when the cycle has no reviews at all", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110774, + "status": "passed", + "title": "is a no-op when the cycle has no reviews at all" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "forceFinish" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) forceFinish transitions ACTIVE -> FINISHED regardless of in-progress reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110791, + "status": "passed", + "title": "transitions ACTIVE -> FINISHED regardless of in-progress reviews" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "delete" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) delete removes the cycle row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110810, + "status": "passed", + "title": "removes the cycle row" + }, + { + "ancestorTitles": [ + "CycleService (integration)", + "delete" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService (integration) delete throws NotFoundException for a missing cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110828, + "status": "passed", + "title": "throws NotFoundException for a missing cycle" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141111043, + "loadTestEnvironmentEnd": 1779141110846, + "loadTestEnvironmentStart": 1779141110844, + "runtime": 197, + "setupAfterEnvEnd": 1779141110869, + "setupAfterEnvStart": 1779141110856, + "setupFilesEnd": 1779141110846, + "setupFilesStart": 1779141110846, + "slow": false, + "start": 1779141110846 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/service/review-scheduler.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewSchedulerService (integration)", + "checkReviewDeadlines" + ], + "duration": 36, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService (integration) checkReviewDeadlines transitions IN_PROGRESS reviews to FINISHED when responseDeadline has passed", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141110913, + "status": "passed", + "title": "transitions IN_PROGRESS reviews to FINISHED when responseDeadline has passed" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService (integration)", + "checkReviewDeadlines" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService (integration) checkReviewDeadlines does nothing when the responseDeadline is in the future", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110950, + "status": "passed", + "title": "does nothing when the responseDeadline is in the future" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService (integration)", + "checkReviewDeadlines" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService (integration) checkReviewDeadlines skips cycles without a responseDeadline", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110971, + "status": "passed", + "title": "skips cycles without a responseDeadline" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService (integration)", + "checkReviewDeadlines" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService (integration) checkReviewDeadlines only affects reviews in IN_PROGRESS stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141110989, + "status": "passed", + "title": "only affects reviews in IN_PROGRESS stage" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService (integration)", + "checkCycleDeadlines" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService (integration) checkCycleDeadlines force-finishes an ACTIVE cycle whose endDate has passed", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111006, + "status": "passed", + "title": "force-finishes an ACTIVE cycle whose endDate has passed" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService (integration)", + "checkCycleDeadlines" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService (integration) checkCycleDeadlines does nothing when both endDate and responseDeadline are in the future", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111025, + "status": "passed", + "title": "does nothing when both endDate and responseDeadline are in the future" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141111353, + "loadTestEnvironmentEnd": 1779141111045, + "loadTestEnvironmentStart": 1779141111043, + "runtime": 308, + "setupAfterEnvEnd": 1779141111068, + "setupAfterEnvStart": 1779141111054, + "setupFilesEnd": 1779141111045, + "setupFilesStart": 1779141111045, + "slow": false, + "start": 1779141111045 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/service/strategic-reports.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "search / checkAccessToAllStrategicReports" + ], + "duration": 32, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) search / checkAccessToAllStrategicReports returns persisted strategic reports for an HR actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111140, + "status": "passed", + "title": "returns persisted strategic reports for an HR actor" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "search / checkAccessToAllStrategicReports" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) search / checkAccessToAllStrategicReports throws ForbiddenException for an unprivileged actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111172, + "status": "passed", + "title": "throws ForbiddenException for an unprivileged actor" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "getById / getByCycleId" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) getById / getByCycleId getById returns the report for an HR actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111187, + "status": "passed", + "title": "getById returns the report for an HR actor" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "getById / getByCycleId" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) getById / getByCycleId getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111211, + "status": "passed", + "title": "getById throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "getById / getByCycleId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) getById / getByCycleId getByCycleId returns the report tied to a cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111226, + "status": "passed", + "title": "getByCycleId returns the report tied to a cycle" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "getById / getByCycleId" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) getById / getByCycleId getByCycleId throws NotFoundException when no report exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111245, + "status": "passed", + "title": "getByCycleId throws NotFoundException when no report exists" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "generateStrategicReportForCycle" + ], + "duration": 38, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) generateStrategicReportForCycle creates a strategic report aggregating individual reports for a cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141111263, + "status": "passed", + "title": "creates a strategic report aggregating individual reports for a cycle" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "generateStrategicReportForCycle" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) generateStrategicReportForCycle returns the existing strategic report if one is already persisted", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111301, + "status": "passed", + "title": "returns the existing strategic report if one is already persisted" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "generateStrategicReportForCycle" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) generateStrategicReportForCycle throws NotFoundException when the cycle is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111321, + "status": "passed", + "title": "throws NotFoundException when the cycle is missing" + }, + { + "ancestorTitles": [ + "StrategicReportingService (integration)", + "generateStrategicReportForCycle" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService (integration) generateStrategicReportForCycle throws NotFoundException when no individual reports exist for the cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111334, + "status": "passed", + "title": "throws NotFoundException when no individual reports exist for the cycle" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 12, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141111685, + "loadTestEnvironmentEnd": 1779141111356, + "loadTestEnvironmentStart": 1779141111354, + "runtime": 329, + "setupAfterEnvEnd": 1779141111377, + "setupAfterEnvStart": 1779141111365, + "setupFilesEnd": 1779141111356, + "setupFilesStart": 1779141111356, + "slow": false, + "start": 1779141111356 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/service/reports.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportingService (integration)", + "search / checkAccessToAllReports" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) search / checkAccessToAllReports returns persisted reports for an HR actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111427, + "status": "passed", + "title": "returns persisted reports for an HR actor" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "search / checkAccessToAllReports" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) search / checkAccessToAllReports returns results when no actor is passed (system call)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111456, + "status": "passed", + "title": "returns results when no actor is passed (system call)" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "search / checkAccessToAllReports" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) search / checkAccessToAllReports throws ForbiddenException for a non-HR / non-Admin actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111481, + "status": "passed", + "title": "throws ForbiddenException for a non-HR / non-Admin actor" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "getById / checkAccessToReport" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) getById / checkAccessToReport returns the report when found and accessor is HR", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111497, + "status": "passed", + "title": "returns the report when found and accessor is HR" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "getById / checkAccessToReport" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) getById / checkAccessToReport returns the report when accessor is the ratee", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111520, + "status": "passed", + "title": "returns the report when accessor is the ratee" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "getById / checkAccessToReport" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) getById / checkAccessToReport throws ForbiddenException when accessor is unrelated to the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111543, + "status": "passed", + "title": "throws ForbiddenException when accessor is unrelated to the review" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "getById / checkAccessToReport" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) getById / checkAccessToReport throws NotFoundException for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111564, + "status": "passed", + "title": "throws NotFoundException for a missing id" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "getByReviewId" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) getByReviewId returns the report tied to a review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111579, + "status": "passed", + "title": "returns the report tied to a review" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "getByReviewId" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) getByReviewId throws NotFoundException when no report exists for a review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111598, + "status": "passed", + "title": "throws NotFoundException when no report exists for a review" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "generateReportForReview" + ], + "duration": 36, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) generateReportForReview creates a report with aggregated analytics for a complete review", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141111614, + "status": "passed", + "title": "creates a report with aggregated analytics for a complete review" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "generateReportForReview" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) generateReportForReview returns the existing report when one already exists for the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111650, + "status": "passed", + "title": "returns the existing report when one already exists for the review" + }, + { + "ancestorTitles": [ + "ReportingService (integration)", + "generateReportForReview" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService (integration) generateReportForReview throws NotFoundException when the review is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111669, + "status": "passed", + "title": "throws NotFoundException when the review is missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 19, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141112010, + "loadTestEnvironmentEnd": 1779141111687, + "loadTestEnvironmentStart": 1779141111685, + "runtime": 323, + "setupAfterEnvEnd": 1779141111708, + "setupAfterEnvStart": 1779141111695, + "setupFilesEnd": 1779141111687, + "setupFilesStart": 1779141111687, + "slow": false, + "start": 1779141111687 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/identity/repository/user.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "UserRepository (integration)", + "create" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) create persists a new user with all attributes", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141111760, + "status": "passed", + "title": "persists a new user with all attributes" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "create" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) create persists assigned roles via the userRoles relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111775, + "status": "passed", + "title": "persists assigned roles via the userRoles relation" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "findById" + ], + "duration": 10, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111789, + "status": "passed", + "title": "returns null for a missing id" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "findById" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) findById returns the user without roles when withRoles is not requested", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141111799, + "status": "passed", + "title": "returns the user without roles when withRoles is not requested" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "findById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) findById returns the user with roles when withRoles is requested", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111811, + "status": "passed", + "title": "returns the user with roles when withRoles is requested" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "findByEmail" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) findByEmail returns the user found by email", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111824, + "status": "passed", + "title": "returns the user found by email" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "findByEmail" + ], + "duration": 10, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) findByEmail returns null when no user has the email", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111836, + "status": "passed", + "title": "returns null when no user has the email" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "search" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) search returns all users with default ordering (ascending by id)", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141111846, + "status": "passed", + "title": "returns all users with default ordering (ascending by id)" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) search filters by lastName (case insensitive substring)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111861, + "status": "passed", + "title": "filters by lastName (case insensitive substring)" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) search filters by status", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111875, + "status": "passed", + "title": "filters by status" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) search filters by the `search` field across firstName / lastName / fullName / email", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141111889, + "status": "passed", + "title": "filters by the `search` field across firstName / lastName / fullName / email" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) search honours descending sort direction on email", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111903, + "status": "passed", + "title": "honours descending sort direction on email" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "updateById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) updateById updates only the fields supplied in the patch", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141111917, + "status": "passed", + "title": "updates only the fields supplied in the patch" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "updateById" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) updateById coerces null teamId/managerId/secondName to null in the database", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141111930, + "status": "passed", + "title": "coerces null teamId/managerId/secondName to null in the database" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "deleteById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) deleteById removes the user row from the database", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141111942, + "status": "passed", + "title": "removes the user row from the database" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "replaceRoles" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) replaceRoles inserts new roles when none existed", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141111955, + "status": "passed", + "title": "inserts new roles when none existed" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "replaceRoles" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) replaceRoles removes roles that are no longer in the requested list", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141111968, + "status": "passed", + "title": "removes roles that are no longer in the requested list" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "replaceRoles" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) replaceRoles clears all roles when given an empty list", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141111981, + "status": "passed", + "title": "clears all roles when given an empty list" + }, + { + "ancestorTitles": [ + "UserRepository (integration)", + "replaceRoles" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserRepository (integration) replaceRoles is idempotent when the same role set is reapplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141111994, + "status": "passed", + "title": "is idempotent when the same role set is reapplied" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 9, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141112272, + "loadTestEnvironmentEnd": 1779141112012, + "loadTestEnvironmentStart": 1779141112011, + "runtime": 260, + "setupAfterEnvEnd": 1779141112035, + "setupAfterEnvStart": 1779141112022, + "setupFilesEnd": 1779141112012, + "setupFilesStart": 1779141112012, + "slow": false, + "start": 1779141112012 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/repository/position-competence-relation.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "link" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) link creates a relation row", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141112078, + "status": "passed", + "title": "creates a relation row" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "link" + ], + "duration": 68, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) link is idempotent: returns the existing row on duplicate link", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141112098, + "status": "passed", + "title": "is idempotent: returns the existing row on duplicate link" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "unlink" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) unlink removes the relation when it exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112166, + "status": "passed", + "title": "removes the relation when it exists" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "unlink" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) unlink does not throw when the relation is absent", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112183, + "status": "passed", + "title": "does not throw when the relation is absent" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "listByCompetence / listByPosition" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) listByCompetence / listByPosition lists all relations for a competence", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112196, + "status": "passed", + "title": "lists all relations for a competence" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "listByCompetence / listByPosition" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) listByCompetence / listByPosition lists all relations for a position", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112211, + "status": "passed", + "title": "lists all relations for a position" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "replaceForCompetence" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) replaceForCompetence keeps the supplied positions and drops the others", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112226, + "status": "passed", + "title": "keeps the supplied positions and drops the others" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "replaceForCompetence" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) replaceForCompetence removes all relations when the new list is empty", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112243, + "status": "passed", + "title": "removes all relations when the new list is empty" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationRepository (integration)", + "deleteAllForCompetence" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationRepository (integration) deleteAllForCompetence removes every relation tied to the competence", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112258, + "status": "passed", + "title": "removes every relation tied to the competence" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141112484, + "loadTestEnvironmentEnd": 1779141112274, + "loadTestEnvironmentStart": 1779141112273, + "runtime": 210, + "setupAfterEnvEnd": 1779141112294, + "setupAfterEnvStart": 1779141112282, + "setupFilesEnd": 1779141112274, + "setupFilesStart": 1779141112274, + "slow": false, + "start": 1779141112274 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/service/report-insight.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportInsightService (integration)", + "getByReportId" + ], + "duration": 37, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService (integration) getByReportId returns insights tied to the report for an HR actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141112346, + "status": "passed", + "title": "returns insights tied to the report for an HR actor" + }, + { + "ancestorTitles": [ + "ReportInsightService (integration)", + "getByReportId" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService (integration) getByReportId returns an empty array when no insights exist (access still checked)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112383, + "status": "passed", + "title": "returns an empty array when no insights exist (access still checked)" + }, + { + "ancestorTitles": [ + "ReportInsightService (integration)", + "getByReportId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService (integration) getByReportId throws NotFoundException when report is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112406, + "status": "passed", + "title": "throws NotFoundException when report is missing" + }, + { + "ancestorTitles": [ + "ReportInsightService (integration)", + "getByReportId" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService (integration) getByReportId throws ForbiddenException for an unrelated actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112425, + "status": "passed", + "title": "throws ForbiddenException for an unrelated actor" + }, + { + "ancestorTitles": [ + "ReportInsightService (integration)", + "getById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService (integration) getById returns the insight when found and the actor has access", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112446, + "status": "passed", + "title": "returns the insight when found and the actor has access" + }, + { + "ancestorTitles": [ + "ReportInsightService (integration)", + "getById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService (integration) getById throws NotFoundException when insight id is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112466, + "status": "passed", + "title": "throws NotFoundException when insight id is missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 9, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141112758, + "loadTestEnvironmentEnd": 1779141112487, + "loadTestEnvironmentStart": 1779141112485, + "runtime": 271, + "setupAfterEnvEnd": 1779141112506, + "setupAfterEnvStart": 1779141112494, + "setupFilesEnd": 1779141112487, + "setupFilesStart": 1779141112487, + "slow": false, + "start": 1779141112487 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/repository/report.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportRepository (integration)", + "create / findById" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) create / findById persists a report with relations defaulting to empty arrays", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141112558, + "status": "passed", + "title": "persists a report with relations defaulting to empty arrays" + }, + { + "ancestorTitles": [ + "ReportRepository (integration)", + "create / findById" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) create / findById findById returns the report with included relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141112587, + "status": "passed", + "title": "findById returns the report with included relations" + }, + { + "ancestorTitles": [ + "ReportRepository (integration)", + "create / findById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) create / findById findById returns null when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112612, + "status": "passed", + "title": "findById returns null when missing" + }, + { + "ancestorTitles": [ + "ReportRepository (integration)", + "findByReviewId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) findByReviewId returns the report attached to a review", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141112626, + "status": "passed", + "title": "returns the report attached to a review" + }, + { + "ancestorTitles": [ + "ReportRepository (integration)", + "findByReviewId" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) findByReviewId returns null when no report exists for a review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112646, + "status": "passed", + "title": "returns null when no report exists for a review" + }, + { + "ancestorTitles": [ + "ReportRepository (integration)", + "search" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) search returns all reports when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112662, + "status": "passed", + "title": "returns all reports when no filter is supplied" + }, + { + "ancestorTitles": [ + "ReportRepository (integration)", + "search" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) search filters by cycleId", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141112686, + "status": "passed", + "title": "filters by cycleId" + }, + { + "ancestorTitles": [ + "ReportRepository (integration)", + "search" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) search filters by respondentCount", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141112710, + "status": "passed", + "title": "filters by respondentCount" + }, + { + "ancestorTitles": [ + "ReportRepository (integration)", + "search" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportRepository (integration) search honours descending sort direction on createdAt", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112734, + "status": "passed", + "title": "honours descending sort direction on createdAt" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141112988, + "loadTestEnvironmentEnd": 1779141112760, + "loadTestEnvironmentStart": 1779141112759, + "runtime": 228, + "setupAfterEnvEnd": 1779141112781, + "setupAfterEnvStart": 1779141112769, + "setupFilesEnd": 1779141112760, + "setupFilesStart": 1779141112760, + "slow": false, + "start": 1779141112760 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/repository/question-template.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "create" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) create persists a template with no position relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141112828, + "status": "passed", + "title": "persists a template with no position relations" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "create" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) create persists the supplied position relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141112849, + "status": "passed", + "title": "persists the supplied position relations" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "findById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) findById returns the template when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112866, + "status": "passed", + "title": "returns the template when found" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "findById" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) findById returns null when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112880, + "status": "passed", + "title": "returns null when missing" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "search" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) search returns all when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112893, + "status": "passed", + "title": "returns all when no filter is supplied" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "search" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) search filters by status", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112908, + "status": "passed", + "title": "filters by status" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) search filters by answerType", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112923, + "status": "passed", + "title": "filters by answerType" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) search honours descending sort on title", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112939, + "status": "passed", + "title": "honours descending sort on title" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "updateById" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) updateById persists the patched scalar fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112955, + "status": "passed", + "title": "persists the patched scalar fields" + }, + { + "ancestorTitles": [ + "QuestionTemplateRepository (integration)", + "deleteById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateRepository (integration) deleteById removes the template row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141112970, + "status": "passed", + "title": "removes the template row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 7, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141113215, + "loadTestEnvironmentEnd": 1779141112990, + "loadTestEnvironmentStart": 1779141112989, + "runtime": 225, + "setupAfterEnvEnd": 1779141113011, + "setupAfterEnvStart": 1779141112998, + "setupFilesEnd": 1779141112990, + "setupFilesStart": 1779141112990, + "slow": false, + "start": 1779141112990 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/repository/competence-question-template-relation.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationRepository (integration)", + "link" + ], + "duration": 26, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationRepository (integration) link creates a relation row", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141113048, + "status": "passed", + "title": "creates a relation row" + }, + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationRepository (integration)", + "link" + ], + "duration": 61, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationRepository (integration) link is idempotent: returns the existing row on duplicate link", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113074, + "status": "passed", + "title": "is idempotent: returns the existing row on duplicate link" + }, + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationRepository (integration)", + "unlink" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationRepository (integration) unlink removes the relation when it exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113135, + "status": "passed", + "title": "removes the relation when it exists" + }, + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationRepository (integration)", + "listByCompetence / listByQuestionTemplate" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationRepository (integration) listByCompetence / listByQuestionTemplate lists relations attached to a competence", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113152, + "status": "passed", + "title": "lists relations attached to a competence" + }, + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationRepository (integration)", + "listByCompetence / listByQuestionTemplate" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationRepository (integration) listByCompetence / listByQuestionTemplate lists relations attached to a question template", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113170, + "status": "passed", + "title": "lists relations attached to a question template" + }, + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationRepository (integration)", + "deleteAllForCompetence / deleteAllForQuestionTemplate" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationRepository (integration) deleteAllForCompetence / deleteAllForQuestionTemplate deleteAllForCompetence wipes the competence side", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113185, + "status": "passed", + "title": "deleteAllForCompetence wipes the competence side" + }, + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationRepository (integration)", + "deleteAllForCompetence / deleteAllForQuestionTemplate" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationRepository (integration) deleteAllForCompetence / deleteAllForQuestionTemplate deleteAllForQuestionTemplate wipes the template side", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113200, + "status": "passed", + "title": "deleteAllForQuestionTemplate wipes the template side" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 8, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141113453, + "loadTestEnvironmentEnd": 1779141113217, + "loadTestEnvironmentStart": 1779141113215, + "runtime": 236, + "setupAfterEnvEnd": 1779141113238, + "setupAfterEnvStart": 1779141113226, + "setupFilesEnd": 1779141113217, + "setupFilesStart": 1779141113217, + "slow": false, + "start": 1779141113217 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/cluster-score.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreRepository (integration)", + "upsert" + ], + "duration": 28, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreRepository (integration) upsert creates a new score row", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141113287, + "status": "passed", + "title": "creates a new score row" + }, + { + "ancestorTitles": [ + "ClusterScoreRepository (integration)", + "upsert" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreRepository (integration) upsert updates the existing row when (clusterId, rateeId) match", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141113315, + "status": "passed", + "title": "updates the existing row when (clusterId, rateeId) match" + }, + { + "ancestorTitles": [ + "ClusterScoreRepository (integration)", + "list" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreRepository (integration) list filters by cycleId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113335, + "status": "passed", + "title": "filters by cycleId" + }, + { + "ancestorTitles": [ + "ClusterScoreRepository (integration)", + "list" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreRepository (integration) list filters by clusterId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113354, + "status": "passed", + "title": "filters by clusterId" + }, + { + "ancestorTitles": [ + "ClusterScoreRepository (integration)", + "getById" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreRepository (integration) getById returns the score with cluster and ratee relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141113373, + "status": "passed", + "title": "returns the score with cluster and ratee relations" + }, + { + "ancestorTitles": [ + "ClusterScoreRepository (integration)", + "getById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreRepository (integration) getById throws NotFoundException for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113395, + "status": "passed", + "title": "throws NotFoundException for a missing id" + }, + { + "ancestorTitles": [ + "ClusterScoreRepository (integration)", + "getByCycleId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreRepository (integration) getByCycleId returns scores tied to the cycle with relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141113414, + "status": "passed", + "title": "returns scores tied to the cycle with relations" + }, + { + "ancestorTitles": [ + "ClusterScoreRepository (integration)", + "deleteById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreRepository (integration) deleteById removes the row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113433, + "status": "passed", + "title": "removes the row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 11, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141113773, + "loadTestEnvironmentEnd": 1779141113455, + "loadTestEnvironmentStart": 1779141113453, + "runtime": 318, + "setupAfterEnvEnd": 1779141113483, + "setupAfterEnvStart": 1779141113470, + "setupFilesEnd": 1779141113455, + "setupFilesStart": 1779141113455, + "slow": false, + "start": 1779141113455 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/review.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "create / findById" + ], + "duration": 28, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) create / findById persists a review with defaults applied", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141113566, + "status": "passed", + "title": "persists a review with defaults applied" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "create / findById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) create / findById findById returns the review when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113594, + "status": "passed", + "title": "findById returns the review when found" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "create / findById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) create / findById findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113612, + "status": "passed", + "title": "findById returns null for a missing id" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "search / listByCycleId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) search / listByCycleId returns all reviews when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113628, + "status": "passed", + "title": "returns all reviews when no filter is supplied" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "search / listByCycleId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) search / listByCycleId filters by stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113647, + "status": "passed", + "title": "filters by stage" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "search / listByCycleId" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) search / listByCycleId filters by hrId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113666, + "status": "passed", + "title": "filters by hrId" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "search / listByCycleId" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) search / listByCycleId listByCycleId returns reviews tied to a cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113683, + "status": "passed", + "title": "listByCycleId returns reviews tied to a cycle" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "search / listByCycleId" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) search / listByCycleId honours descending sort on rateeFullName", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113701, + "status": "passed", + "title": "honours descending sort on rateeFullName" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "updateById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) updateById persists patched scalar fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113719, + "status": "passed", + "title": "persists patched scalar fields" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "updateById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) updateById updates the stage when patched", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113736, + "status": "passed", + "title": "updates the stage when patched" + }, + { + "ancestorTitles": [ + "ReviewRepository (integration)", + "deleteById" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewRepository (integration) deleteById cascades cleanup of dependent rows in a single transaction", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113752, + "status": "passed", + "title": "cascades cleanup of dependent rows in a single transaction" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141113992, + "loadTestEnvironmentEnd": 1779141113775, + "loadTestEnvironmentStart": 1779141113773, + "runtime": 217, + "setupAfterEnvEnd": 1779141113797, + "setupAfterEnvStart": 1779141113784, + "setupFilesEnd": 1779141113775, + "setupFilesStart": 1779141113775, + "slow": false, + "start": 1779141113775 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/service/report-comment.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportCommentService (integration)", + "create" + ], + "duration": 31, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService (integration) create persists a comment via the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141113848, + "status": "passed", + "title": "persists a comment via the repository" + }, + { + "ancestorTitles": [ + "ReportCommentService (integration)", + "getByReportId" + ], + "duration": 27, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService (integration) getByReportId returns comments tied to a report for an HR actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113879, + "status": "passed", + "title": "returns comments tied to a report for an HR actor" + }, + { + "ancestorTitles": [ + "ReportCommentService (integration)", + "getByReportId" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService (integration) getByReportId throws NotFoundException when no comments exist for the report", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113906, + "status": "passed", + "title": "throws NotFoundException when no comments exist for the report" + }, + { + "ancestorTitles": [ + "ReportCommentService (integration)", + "getByReportId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService (integration) getByReportId delegates report-access check (throws when report is missing)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113935, + "status": "passed", + "title": "delegates report-access check (throws when report is missing)" + }, + { + "ancestorTitles": [ + "ReportCommentService (integration)", + "getById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService (integration) getById returns the comment when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113954, + "status": "passed", + "title": "returns the comment when found" + }, + { + "ancestorTitles": [ + "ReportCommentService (integration)", + "getById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService (integration) getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141113974, + "status": "passed", + "title": "throws NotFoundException when missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141114175, + "loadTestEnvironmentEnd": 1779141113994, + "loadTestEnvironmentStart": 1779141113993, + "runtime": 180, + "setupAfterEnvEnd": 1779141114014, + "setupAfterEnvStart": 1779141114002, + "setupFilesEnd": 1779141113995, + "setupFilesStart": 1779141113995, + "slow": false, + "start": 1779141113995 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/repository/strategic-report-insight.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportInsightRepository (integration)", + "createMany" + ], + "duration": 27, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightRepository (integration) createMany persists multiple insight rows", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114063, + "status": "passed", + "title": "persists multiple insight rows" + }, + { + "ancestorTitles": [ + "StrategicReportInsightRepository (integration)", + "createMany" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightRepository (integration) createMany is a no-op for an empty list", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114090, + "status": "passed", + "title": "is a no-op for an empty list" + }, + { + "ancestorTitles": [ + "StrategicReportInsightRepository (integration)", + "findByStrategicReportId" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightRepository (integration) findByStrategicReportId returns insights in ascending id order", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114107, + "status": "passed", + "title": "returns insights in ascending id order" + }, + { + "ancestorTitles": [ + "StrategicReportInsightRepository (integration)", + "findByStrategicReportId" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightRepository (integration) findByStrategicReportId returns an empty array when no insights exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114125, + "status": "passed", + "title": "returns an empty array when no insights exist" + }, + { + "ancestorTitles": [ + "StrategicReportInsightRepository (integration)", + "findById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightRepository (integration) findById returns the insight when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114141, + "status": "passed", + "title": "returns the insight when found" + }, + { + "ancestorTitles": [ + "StrategicReportInsightRepository (integration)", + "findById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114158, + "status": "passed", + "title": "returns null for a missing id" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141114431, + "loadTestEnvironmentEnd": 1779141114177, + "loadTestEnvironmentStart": 1779141114175, + "runtime": 254, + "setupAfterEnvEnd": 1779141114196, + "setupAfterEnvStart": 1779141114184, + "setupFilesEnd": 1779141114177, + "setupFilesStart": 1779141114177, + "slow": false, + "start": 1779141114177 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/answer.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "create / findById / list" + ], + "duration": 26, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) create / findById / list persists a numerical answer", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141114245, + "status": "passed", + "title": "persists a numerical answer" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "create / findById / list" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) create / findById / list persists a text answer", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141114271, + "status": "passed", + "title": "persists a text answer" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "create / findById / list" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) create / findById / list findById returns the answer when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114290, + "status": "passed", + "title": "findById returns the answer when found" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "create / findById / list" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) create / findById / list findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114308, + "status": "passed", + "title": "findById returns null for a missing id" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "create / findById / list" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) create / findById / list list filters by reviewId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114325, + "status": "passed", + "title": "list filters by reviewId" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "create / findById / list" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) create / findById / list list filters by respondentCategory", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141114342, + "status": "passed", + "title": "list filters by respondentCategory" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "create / findById / list" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) create / findById / list list honours sort direction", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114360, + "status": "passed", + "title": "list honours sort direction" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "getAnswersCountByRespondentCategories" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) getAnswersCountByRespondentCategories returns an empty list when there are no answers", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114378, + "status": "passed", + "title": "returns an empty list when there are no answers" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "getAnswersCountByRespondentCategories" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) getAnswersCountByRespondentCategories counts answers per respondent and aggregates by category", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114394, + "status": "passed", + "title": "counts answers per respondent and aggregates by category" + }, + { + "ancestorTitles": [ + "AnswerRepository (integration)", + "deleteById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerRepository (integration) deleteById removes the answer row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114412, + "status": "passed", + "title": "removes the answer row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 8, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141114688, + "loadTestEnvironmentEnd": 1779141114433, + "loadTestEnvironmentStart": 1779141114431, + "runtime": 255, + "setupAfterEnvEnd": 1779141114454, + "setupAfterEnvStart": 1779141114441, + "setupFilesEnd": 1779141114433, + "setupFilesStart": 1779141114433, + "slow": false, + "start": 1779141114433 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/respondent.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RespondentRepository (integration)", + "create / getById" + ], + "duration": 34, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentRepository (integration) create / getById persists a respondent with defaults applied", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141114504, + "status": "passed", + "title": "persists a respondent with defaults applied" + }, + { + "ancestorTitles": [ + "RespondentRepository (integration)", + "create / getById" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentRepository (integration) create / getById getById returns the respondent when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114538, + "status": "passed", + "title": "getById returns the respondent when found" + }, + { + "ancestorTitles": [ + "RespondentRepository (integration)", + "create / getById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentRepository (integration) create / getById getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114559, + "status": "passed", + "title": "getById throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "RespondentRepository (integration)", + "listByReview" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentRepository (integration) listByReview returns respondents filtered by category", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141114577, + "status": "passed", + "title": "returns respondents filtered by category" + }, + { + "ancestorTitles": [ + "RespondentRepository (integration)", + "listByReview" + ], + "duration": 30, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentRepository (integration) listByReview returns respondents filtered by responseStatus", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141114598, + "status": "passed", + "title": "returns respondents filtered by responseStatus" + }, + { + "ancestorTitles": [ + "RespondentRepository (integration)", + "listByReview" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentRepository (integration) listByReview honours sort direction", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114628, + "status": "passed", + "title": "honours sort direction" + }, + { + "ancestorTitles": [ + "RespondentRepository (integration)", + "updateById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentRepository (integration) updateById persists patched fields with enum conversion", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141114649, + "status": "passed", + "title": "persists patched fields with enum conversion" + }, + { + "ancestorTitles": [ + "RespondentRepository (integration)", + "deleteById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentRepository (integration) deleteById removes the respondent row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114668, + "status": "passed", + "title": "removes the respondent row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 14, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141114947, + "loadTestEnvironmentEnd": 1779141114690, + "loadTestEnvironmentStart": 1779141114689, + "runtime": 257, + "setupAfterEnvEnd": 1779141114711, + "setupAfterEnvStart": 1779141114699, + "setupFilesEnd": 1779141114690, + "setupFilesStart": 1779141114690, + "slow": false, + "start": 1779141114690 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/organisation/service/position.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionService (integration)", + "create" + ], + "duration": 26, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) create persists a new position with the supplied attributes", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141114749, + "status": "passed", + "title": "persists a new position with the supplied attributes" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "create" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) create coerces a missing description to null in the database", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114776, + "status": "passed", + "title": "coerces a missing description to null in the database" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) search returns all positions when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114789, + "status": "passed", + "title": "returns all positions when no filter is supplied" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) search filters by title (case insensitive substring)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114803, + "status": "passed", + "title": "filters by title (case insensitive substring)" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "getById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) getById returns the position when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114817, + "status": "passed", + "title": "returns the position when found" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "getById" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114830, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "update" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) update forwards the patch and persists the changes", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141114842, + "status": "passed", + "title": "forwards the patch and persists the changes" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "update" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) update throws NotFoundException when the position does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114855, + "status": "passed", + "title": "throws NotFoundException when the position does not exist" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "delete" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) delete removes the position when nothing is linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114867, + "status": "passed", + "title": "removes the position when nothing is linked" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "delete" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) delete rejects deletion when the position has competence relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141114881, + "status": "passed", + "title": "rejects deletion when the position has competence relations" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "delete" + ], + "duration": 11, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) delete throws NotFoundException when deleting a missing position", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114896, + "status": "passed", + "title": "throws NotFoundException when deleting a missing position" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "listCompetences" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) listCompetences returns competence ids linked to the position", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114908, + "status": "passed", + "title": "returns competence ids linked to the position" + }, + { + "ancestorTitles": [ + "PositionService (integration)", + "listCompetences" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) listCompetences returns an empty array when the position has no competences", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141114922, + "status": "passed", + "title": "returns an empty array when the position has no competences" + }, + { + "ancestorTitles": [ + "PositionService (integration)" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService (integration) round-trips a domain through the database", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141114934, + "status": "passed", + "title": "round-trips a domain through the database" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141115231, + "loadTestEnvironmentEnd": 1779141114949, + "loadTestEnvironmentStart": 1779141114948, + "runtime": 282, + "setupAfterEnvEnd": 1779141114973, + "setupAfterEnvStart": 1779141114960, + "setupFilesEnd": 1779141114949, + "setupFilesStart": 1779141114949, + "slow": false, + "start": 1779141114949 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/organisation/service/position-hierarchy.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "link" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) link persists a superior → subordinate relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115022, + "status": "passed", + "title": "persists a superior → subordinate relation" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "link" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) link rejects self-linking", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115046, + "status": "passed", + "title": "rejects self-linking" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "link" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) link throws NotFoundException when either position is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115061, + "status": "passed", + "title": "throws NotFoundException when either position is missing" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "link" + ], + "duration": 63, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) link is idempotent for duplicate links", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115075, + "status": "passed", + "title": "is idempotent for duplicate links" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "unlink" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) unlink removes an existing link", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115138, + "status": "passed", + "title": "removes an existing link" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "unlink" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) unlink does nothing when the link did not exist (no error)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115156, + "status": "passed", + "title": "does nothing when the link did not exist (no error)" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "listSubordinates" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) listSubordinates returns every subordinate position of the given superior", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141115170, + "status": "passed", + "title": "returns every subordinate position of the given superior" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "listSubordinates" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) listSubordinates returns an empty array when there are no subordinates", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115188, + "status": "passed", + "title": "returns an empty array when there are no subordinates" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "listSuperiors" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) listSuperiors returns every superior of the given subordinate", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115202, + "status": "passed", + "title": "returns every superior of the given subordinate" + }, + { + "ancestorTitles": [ + "PositionHierarchyService (integration)", + "listSuperiors" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService (integration) listSuperiors returns an empty array when there are no superiors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115217, + "status": "passed", + "title": "returns an empty array when there are no superiors" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 11, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141115479, + "loadTestEnvironmentEnd": 1779141115233, + "loadTestEnvironmentStart": 1779141115232, + "runtime": 246, + "setupAfterEnvEnd": 1779141115252, + "setupAfterEnvStart": 1779141115241, + "setupFilesEnd": 1779141115233, + "setupFilesStart": 1779141115233, + "slow": false, + "start": 1779141115233 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/cycle.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleRepository (integration)", + "create / findById" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) create / findById persists a new cycle with defaults applied", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141115302, + "status": "passed", + "title": "persists a new cycle with defaults applied" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "create / findById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) create / findById findById returns the cycle when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115325, + "status": "passed", + "title": "findById returns the cycle when found" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "create / findById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) create / findById findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115341, + "status": "passed", + "title": "findById returns null for a missing id" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) search returns all cycles when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115354, + "status": "passed", + "title": "returns all cycles when no filter is supplied" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "search" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) search filters by title (case insensitive substring)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115370, + "status": "passed", + "title": "filters by title (case insensitive substring)" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "search" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) search filters by stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115389, + "status": "passed", + "title": "filters by stage" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) search filters by isActive", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115404, + "status": "passed", + "title": "filters by isActive" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "search" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) search honours descending sort on title", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115420, + "status": "passed", + "title": "honours descending sort on title" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "updateById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) updateById persists the patched scalar fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115435, + "status": "passed", + "title": "persists the patched scalar fields" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "updateById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) updateById updates the cycle stage when patched", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115449, + "status": "passed", + "title": "updates the cycle stage when patched" + }, + { + "ancestorTitles": [ + "CycleRepository (integration)", + "deleteById" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleRepository (integration) deleteById removes the cycle row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115463, + "status": "passed", + "title": "removes the cycle row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 9, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141115795, + "loadTestEnvironmentEnd": 1779141115481, + "loadTestEnvironmentStart": 1779141115479, + "runtime": 314, + "setupAfterEnvEnd": 1779141115502, + "setupAfterEnvStart": 1779141115490, + "setupFilesEnd": 1779141115481, + "setupFilesStart": 1779141115481, + "slow": false, + "start": 1779141115481 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/repository/strategic-report.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "create / findById" + ], + "duration": 78, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) create / findById persists a strategic report with empty relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141115564, + "status": "passed", + "title": "persists a strategic report with empty relations" + }, + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "create / findById" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) create / findById findById returns the report with included relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115642, + "status": "passed", + "title": "findById returns the report with included relations" + }, + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "create / findById" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) create / findById findById returns null when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115664, + "status": "passed", + "title": "findById returns null when missing" + }, + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "findByCycleId" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) findByCycleId returns the most recently created report for a cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115679, + "status": "passed", + "title": "returns the most recently created report for a cycle" + }, + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "findByCycleId" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) findByCycleId returns null when no strategic report exists for a cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115699, + "status": "passed", + "title": "returns null when no strategic report exists for a cycle" + }, + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "search" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) search returns all reports when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115714, + "status": "passed", + "title": "returns all reports when no filter is supplied" + }, + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "search" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) search filters by cycleId", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115735, + "status": "passed", + "title": "filters by cycleId" + }, + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "search" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) search filters by rateeIds (hasSome)", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115754, + "status": "passed", + "title": "filters by rateeIds (hasSome)" + }, + { + "ancestorTitles": [ + "StrategicReportRepository (integration)", + "search" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportRepository (integration) search honours descending sort on createdAt", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115775, + "status": "passed", + "title": "honours descending sort on createdAt" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 8, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141116042, + "loadTestEnvironmentEnd": 1779141115798, + "loadTestEnvironmentStart": 1779141115796, + "runtime": 244, + "setupAfterEnvEnd": 1779141115819, + "setupAfterEnvStart": 1779141115807, + "setupFilesEnd": 1779141115798, + "setupFilesStart": 1779141115798, + "slow": false, + "start": 1779141115798 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/repository/position-question-template-relation.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationRepository (integration)", + "link" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationRepository (integration) link creates a relation row", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141115857, + "status": "passed", + "title": "creates a relation row" + }, + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationRepository (integration)", + "link" + ], + "duration": 61, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationRepository (integration) link is idempotent: returns the existing row on duplicate link", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141115882, + "status": "passed", + "title": "is idempotent: returns the existing row on duplicate link" + }, + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationRepository (integration)", + "unlink" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationRepository (integration) unlink removes the relation when it exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115943, + "status": "passed", + "title": "removes the relation when it exists" + }, + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationRepository (integration)", + "listByQuestion" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationRepository (integration) listByQuestion returns all relations attached to a question template", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115961, + "status": "passed", + "title": "returns all relations attached to a question template" + }, + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationRepository (integration)", + "replace" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationRepository (integration) replace keeps the supplied positions and drops the others", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115977, + "status": "passed", + "title": "keeps the supplied positions and drops the others" + }, + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationRepository (integration)", + "replace" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationRepository (integration) replace removes all relations when the new list is empty", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141115995, + "status": "passed", + "title": "removes all relations when the new list is empty" + }, + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationRepository (integration)", + "replace" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationRepository (integration) replace deduplicates the input list", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116011, + "status": "passed", + "title": "deduplicates the input list" + }, + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationRepository (integration)", + "deleteAllForQuestionTemplate" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationRepository (integration) deleteAllForQuestionTemplate removes every relation tied to the template", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116026, + "status": "passed", + "title": "removes every relation tied to the template" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141116298, + "loadTestEnvironmentEnd": 1779141116044, + "loadTestEnvironmentStart": 1779141116043, + "runtime": 254, + "setupAfterEnvEnd": 1779141116067, + "setupAfterEnvStart": 1779141116051, + "setupFilesEnd": 1779141116044, + "setupFilesStart": 1779141116044, + "slow": false, + "start": 1779141116044 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/notifications/service/review-email-notification.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyUserWelcome" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyUserWelcome sends a welcome email and persists a sent NotificationLog row", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141116116, + "status": "passed", + "title": "sends a welcome email and persists a sent NotificationLog row" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyUserWelcome" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyUserWelcome skips dispatch when the user already has a sent welcome log", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116142, + "status": "passed", + "title": "skips dispatch when the user already has a sent welcome log" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyUserWelcome" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyUserWelcome returns 0 when the user is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116158, + "status": "passed", + "title": "returns 0 when the user is missing" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyRateeSelfAssessment" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyRateeSelfAssessment sends one email to the ratee and writes a sent log tied to the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141116172, + "status": "passed", + "title": "sends one email to the ratee and writes a sent log tied to the review" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyRateeSelfAssessment" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyRateeSelfAssessment returns 0 when the review is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116192, + "status": "passed", + "title": "returns 0 when the review is missing" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyRespondents" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyRespondents emails every respondent and writes one log row per recipient", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141116205, + "status": "passed", + "title": "emails every respondent and writes one log row per recipient" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyRespondents" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyRespondents returns 0 when the review is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116228, + "status": "passed", + "title": "returns 0 when the review is missing" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyHrReportReady" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyHrReportReady emails the HR for the review and persists a sent log", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141116242, + "status": "passed", + "title": "emails the HR for the review and persists a sent log" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "notifyReviewers" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) notifyReviewers emails every reviewer attached to the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116259, + "status": "passed", + "title": "emails every reviewer attached to the review" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService (integration)", + "failure handling" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService (integration) failure handling records the error message and reports 0 sent when the mailer throws", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141116277, + "status": "passed", + "title": "records the error message and reports 0 sent when the mailer throws" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 12, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141116546, + "loadTestEnvironmentEnd": 1779141116300, + "loadTestEnvironmentStart": 1779141116299, + "runtime": 246, + "setupAfterEnvEnd": 1779141116321, + "setupAfterEnvStart": 1779141116308, + "setupFilesEnd": 1779141116300, + "setupFilesStart": 1779141116300, + "slow": false, + "start": 1779141116300 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/service/cluster.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterService (integration)", + "create" + ], + "duration": 33, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) create persists a cluster bound to the competence", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141116365, + "status": "passed", + "title": "persists a cluster bound to the competence" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "create" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) create rejects creation when lowerBound > upperBound", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116398, + "status": "passed", + "title": "rejects creation when lowerBound > upperBound" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "create" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) create throws NotFoundException when the competence is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116411, + "status": "passed", + "title": "throws NotFoundException when the competence is missing" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) search returns all clusters when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116424, + "status": "passed", + "title": "returns all clusters when no filter is supplied" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) search filters by title (case insensitive substring)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116439, + "status": "passed", + "title": "filters by title (case insensitive substring)" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "getById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) getById returns the cluster when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116453, + "status": "passed", + "title": "returns the cluster when found" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "getById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) getById throws NotFoundException for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116466, + "status": "passed", + "title": "throws NotFoundException for a missing id" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "update" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) update persists patched fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116479, + "status": "passed", + "title": "persists patched fields" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "update" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) update rejects when patched bounds are inverted", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116493, + "status": "passed", + "title": "rejects when patched bounds are inverted" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "update" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) update validates the new competenceId exists when changed", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116506, + "status": "passed", + "title": "validates the new competenceId exists when changed" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "delete" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) delete removes the cluster row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116519, + "status": "passed", + "title": "removes the cluster row" + }, + { + "ancestorTitles": [ + "ClusterService (integration)", + "delete" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService (integration) delete throws NotFoundException for a missing cluster", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116533, + "status": "passed", + "title": "throws NotFoundException for a missing cluster" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 9, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141116798, + "loadTestEnvironmentEnd": 1779141116548, + "loadTestEnvironmentStart": 1779141116547, + "runtime": 250, + "setupAfterEnvEnd": 1779141116570, + "setupAfterEnvStart": 1779141116558, + "setupFilesEnd": 1779141116548, + "setupFilesStart": 1779141116548, + "slow": false, + "start": 1779141116548 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/cluster-score-analytics.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "upsert" + ], + "duration": 26, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) upsert creates a new analytics row", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116629, + "status": "passed", + "title": "creates a new analytics row" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "upsert" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) upsert updates the existing row when (cycleId, clusterId) match", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141116656, + "status": "passed", + "title": "updates the existing row when (cycleId, clusterId) match" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "findById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) findById returns the row when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116674, + "status": "passed", + "title": "returns the row when found" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "findById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116691, + "status": "passed", + "title": "returns null for a missing id" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "search" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) search filters by cycleId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116707, + "status": "passed", + "title": "filters by cycleId" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) search filters by clusterId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116733, + "status": "passed", + "title": "filters by clusterId" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "updateById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) updateById persists patched fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116749, + "status": "passed", + "title": "persists patched fields" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "getByCycleId" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) getByCycleId returns analytics tied to a cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116765, + "status": "passed", + "title": "returns analytics tied to a cycle" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsRepository (integration)", + "deleteById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsRepository (integration) deleteById removes the row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116780, + "status": "passed", + "title": "removes the row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141116983, + "loadTestEnvironmentEnd": 1779141116800, + "loadTestEnvironmentStart": 1779141116799, + "runtime": 183, + "setupAfterEnvEnd": 1779141116821, + "setupAfterEnvStart": 1779141116809, + "setupFilesEnd": 1779141116800, + "setupFilesStart": 1779141116800, + "slow": false, + "start": 1779141116800 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/reviewer.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewerRepository (integration)", + "create" + ], + "duration": 38, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerRepository (integration) create persists a reviewer row", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116866, + "status": "passed", + "title": "persists a reviewer row" + }, + { + "ancestorTitles": [ + "ReviewerRepository (integration)", + "listByReview" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerRepository (integration) listByReview returns reviewers for a given review", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141116904, + "status": "passed", + "title": "returns reviewers for a given review" + }, + { + "ancestorTitles": [ + "ReviewerRepository (integration)", + "listByReview" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerRepository (integration) listByReview filters by fullName (case insensitive)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116923, + "status": "passed", + "title": "filters by fullName (case insensitive)" + }, + { + "ancestorTitles": [ + "ReviewerRepository (integration)", + "listByReview" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerRepository (integration) listByReview honours sort direction", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116943, + "status": "passed", + "title": "honours sort direction" + }, + { + "ancestorTitles": [ + "ReviewerRepository (integration)", + "deleteById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerRepository (integration) deleteById removes the reviewer row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141116963, + "status": "passed", + "title": "removes the reviewer row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141117218, + "loadTestEnvironmentEnd": 1779141116984, + "loadTestEnvironmentStart": 1779141116983, + "runtime": 234, + "setupAfterEnvEnd": 1779141117004, + "setupAfterEnvStart": 1779141116993, + "setupFilesEnd": 1779141116984, + "setupFilesStart": 1779141116984, + "slow": false, + "start": 1779141116984 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/question.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "create / findById" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) create / findById persists a new question with defaults applied", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141117056, + "status": "passed", + "title": "persists a new question with defaults applied" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "create / findById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) create / findById findById returns the row when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117079, + "status": "passed", + "title": "findById returns the row when found" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "create / findById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) create / findById findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117095, + "status": "passed", + "title": "findById returns null for a missing id" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) search returns all questions when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117109, + "status": "passed", + "title": "returns all questions when no filter is supplied" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) search filters by cycleId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117125, + "status": "passed", + "title": "filters by cycleId" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "search" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) search filters by answerType", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117141, + "status": "passed", + "title": "filters by answerType" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) search filters by isForSelfassessment = true", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117156, + "status": "passed", + "title": "filters by isForSelfassessment = true" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) search filters by title (case insensitive substring)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117172, + "status": "passed", + "title": "filters by title (case insensitive substring)" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "search" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) search honours descending sort on title", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117186, + "status": "passed", + "title": "honours descending sort on title" + }, + { + "ancestorTitles": [ + "QuestionRepository (integration)", + "deleteById" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionRepository (integration) deleteById removes the question row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117202, + "status": "passed", + "title": "removes the question row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141117408, + "loadTestEnvironmentEnd": 1779141117220, + "loadTestEnvironmentStart": 1779141117219, + "runtime": 188, + "setupAfterEnvEnd": 1779141117242, + "setupAfterEnvStart": 1779141117229, + "setupFilesEnd": 1779141117220, + "setupFilesStart": 1779141117220, + "slow": false, + "start": 1779141117220 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/repository/strategic-report-analytics.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportAnalyticsRepository (integration)", + "createMany" + ], + "duration": 31, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsRepository (integration) createMany persists multiple analytics rows", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117292, + "status": "passed", + "title": "persists multiple analytics rows" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsRepository (integration)", + "createMany" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsRepository (integration) createMany is a no-op for an empty list", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117323, + "status": "passed", + "title": "is a no-op for an empty list" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsRepository (integration)", + "findByStrategicReportId" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsRepository (integration) findByStrategicReportId returns analytics in ascending id order", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141117340, + "status": "passed", + "title": "returns analytics in ascending id order" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsRepository (integration)", + "findByStrategicReportId" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsRepository (integration) findByStrategicReportId returns an empty array when no analytics exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117358, + "status": "passed", + "title": "returns an empty array when no analytics exist" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsRepository (integration)", + "findById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsRepository (integration) findById returns the analytics row when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117374, + "status": "passed", + "title": "returns the analytics row when found" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsRepository (integration)", + "findById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117391, + "status": "passed", + "title": "returns null for a missing id" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141117638, + "loadTestEnvironmentEnd": 1779141117410, + "loadTestEnvironmentStart": 1779141117409, + "runtime": 228, + "setupAfterEnvEnd": 1779141117430, + "setupAfterEnvStart": 1779141117418, + "setupFilesEnd": 1779141117410, + "setupFilesStart": 1779141117410, + "slow": false, + "start": 1779141117410 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/service/startegic-report-insight.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StartegicReportInsightService (integration)", + "getByStrategicReportId" + ], + "duration": 47, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService (integration) getByStrategicReportId returns insights tied to the strategic report for an HR actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141117488, + "status": "passed", + "title": "returns insights tied to the strategic report for an HR actor" + }, + { + "ancestorTitles": [ + "StartegicReportInsightService (integration)", + "getByStrategicReportId" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService (integration) getByStrategicReportId returns an empty array when no insights exist (access still checked)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117535, + "status": "passed", + "title": "returns an empty array when no insights exist (access still checked)" + }, + { + "ancestorTitles": [ + "StartegicReportInsightService (integration)", + "getByStrategicReportId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService (integration) getByStrategicReportId throws NotFoundException when strategic report is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117558, + "status": "passed", + "title": "throws NotFoundException when strategic report is missing" + }, + { + "ancestorTitles": [ + "StartegicReportInsightService (integration)", + "getByStrategicReportId" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService (integration) getByStrategicReportId throws ForbiddenException for an unprivileged actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117577, + "status": "passed", + "title": "throws ForbiddenException for an unprivileged actor" + }, + { + "ancestorTitles": [ + "StartegicReportInsightService (integration)", + "getById" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService (integration) getById returns the insight when found and the actor has access", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117597, + "status": "passed", + "title": "returns the insight when found and the actor has access" + }, + { + "ancestorTitles": [ + "StartegicReportInsightService (integration)", + "getById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService (integration) getById throws NotFoundException when insight id is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117619, + "status": "passed", + "title": "throws NotFoundException when insight id is missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 7, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141117872, + "loadTestEnvironmentEnd": 1779141117640, + "loadTestEnvironmentStart": 1779141117638, + "runtime": 232, + "setupAfterEnvEnd": 1779141117659, + "setupAfterEnvStart": 1779141117647, + "setupFilesEnd": 1779141117640, + "setupFilesStart": 1779141117640, + "slow": false, + "start": 1779141117640 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/listeners/respondent-status.listener.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration)", + "RespondentStatusListener.handleRespondentStatusChanged" + ], + "duration": 32, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration) RespondentStatusListener.handleRespondentStatusChanged does nothing when status is not COMPLETED or CANCELED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117710, + "status": "passed", + "title": "does nothing when status is not COMPLETED or CANCELED" + }, + { + "ancestorTitles": [ + "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration)", + "RespondentStatusListener.handleRespondentStatusChanged" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration) RespondentStatusListener.handleRespondentStatusChanged completes the review when all respondents are COMPLETED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117742, + "status": "passed", + "title": "completes the review when all respondents are COMPLETED" + }, + { + "ancestorTitles": [ + "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration)", + "RespondentStatusListener.handleRespondentStatusChanged" + ], + "duration": 26, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration) RespondentStatusListener.handleRespondentStatusChanged does not complete the review when some respondents are still PENDING", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117771, + "status": "passed", + "title": "does not complete the review when some respondents are still PENDING" + }, + { + "ancestorTitles": [ + "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration)", + "SelfAssessmentCompletedListener.handleRespondentCompleted" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration) SelfAssessmentCompletedListener.handleRespondentCompleted advances the review SELF_ASSESSMENT → IN_PROGRESS when the ratee completes self-assessment", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117797, + "status": "passed", + "title": "advances the review SELF_ASSESSMENT → IN_PROGRESS when the ratee completes self-assessment" + }, + { + "ancestorTitles": [ + "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration)", + "SelfAssessmentCompletedListener.handleRespondentCompleted" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration) SelfAssessmentCompletedListener.handleRespondentCompleted skips the transition when the completed respondent is not a SELF_ASSESSMENT", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117818, + "status": "passed", + "title": "skips the transition when the completed respondent is not a SELF_ASSESSMENT" + }, + { + "ancestorTitles": [ + "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration)", + "SelfAssessmentCompletedListener.handleRespondentCompleted" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration) SelfAssessmentCompletedListener.handleRespondentCompleted skips the transition when the review is not in SELF_ASSESSMENT stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117837, + "status": "passed", + "title": "skips the transition when the review is not in SELF_ASSESSMENT stage" + }, + { + "ancestorTitles": [ + "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration)", + "SelfAssessmentCompletedListener.handleRespondentCompleted" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener / SelfAssessmentCompletedListener (feedback360, integration) SelfAssessmentCompletedListener.handleRespondentCompleted does nothing when toStatus is not COMPLETED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141117854, + "status": "passed", + "title": "does nothing when toStatus is not COMPLETED" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 7, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141118090, + "loadTestEnvironmentEnd": 1779141117874, + "loadTestEnvironmentStart": 1779141117873, + "runtime": 216, + "setupAfterEnvEnd": 1779141117894, + "setupAfterEnvStart": 1779141117882, + "setupFilesEnd": 1779141117874, + "setupFilesStart": 1779141117874, + "slow": false, + "start": 1779141117874 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/organisation/repository/position-hierarchy.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionHierarchyRepository (integration)", + "link" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyRepository (integration) link persists a hierarchy relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141117938, + "status": "passed", + "title": "persists a hierarchy relation" + }, + { + "ancestorTitles": [ + "PositionHierarchyRepository (integration)", + "link" + ], + "duration": 61, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyRepository (integration) link is idempotent: returns the existing row on duplicate link", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141117957, + "status": "passed", + "title": "is idempotent: returns the existing row on duplicate link" + }, + { + "ancestorTitles": [ + "PositionHierarchyRepository (integration)", + "unlink" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyRepository (integration) unlink removes the relation when it exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118019, + "status": "passed", + "title": "removes the relation when it exists" + }, + { + "ancestorTitles": [ + "PositionHierarchyRepository (integration)", + "unlink" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyRepository (integration) unlink does not throw when the relation is absent", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118035, + "status": "passed", + "title": "does not throw when the relation is absent" + }, + { + "ancestorTitles": [ + "PositionHierarchyRepository (integration)", + "listSubordinates / listSuperiors" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyRepository (integration) listSubordinates / listSuperiors listSubordinates returns relations where the position is superior", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141118048, + "status": "passed", + "title": "listSubordinates returns relations where the position is superior" + }, + { + "ancestorTitles": [ + "PositionHierarchyRepository (integration)", + "listSubordinates / listSuperiors" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyRepository (integration) listSubordinates / listSuperiors listSuperiors returns relations where the position is subordinate", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141118062, + "status": "passed", + "title": "listSuperiors returns relations where the position is subordinate" + }, + { + "ancestorTitles": [ + "PositionHierarchyRepository (integration)", + "listSubordinates / listSuperiors" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyRepository (integration) listSubordinates / listSuperiors returns an empty array when no relations exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141118075, + "status": "passed", + "title": "returns an empty array when no relations exist" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141118314, + "loadTestEnvironmentEnd": 1779141118091, + "loadTestEnvironmentStart": 1779141118090, + "runtime": 222, + "setupAfterEnvEnd": 1779141118111, + "setupAfterEnvStart": 1779141118099, + "setupFilesEnd": 1779141118092, + "setupFilesStart": 1779141118092, + "slow": false, + "start": 1779141118092 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/service/strategic-report-analytics.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportAnalyticsService (integration)", + "getByStrategicReportId" + ], + "duration": 46, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService (integration) getByStrategicReportId returns analytics tied to the strategic report for an HR actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141118163, + "status": "passed", + "title": "returns analytics tied to the strategic report for an HR actor" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsService (integration)", + "getByStrategicReportId" + ], + "duration": 25, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService (integration) getByStrategicReportId returns an empty array when no analytics exist (access still checked)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118209, + "status": "passed", + "title": "returns an empty array when no analytics exist (access still checked)" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsService (integration)", + "getByStrategicReportId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService (integration) getByStrategicReportId throws NotFoundException when strategic report is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118234, + "status": "passed", + "title": "throws NotFoundException when strategic report is missing" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsService (integration)", + "getByStrategicReportId" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService (integration) getByStrategicReportId throws ForbiddenException for an unprivileged actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118253, + "status": "passed", + "title": "throws ForbiddenException for an unprivileged actor" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsService (integration)", + "getById" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService (integration) getById returns the analytics row when found and the actor has access", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118275, + "status": "passed", + "title": "returns the analytics row when found and the actor has access" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsService (integration)", + "getById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService (integration) getById throws NotFoundException when analytics id is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118295, + "status": "passed", + "title": "throws NotFoundException when analytics id is missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141118570, + "loadTestEnvironmentEnd": 1779141118316, + "loadTestEnvironmentStart": 1779141118315, + "runtime": 254, + "setupAfterEnvEnd": 1779141118335, + "setupAfterEnvStart": 1779141118324, + "setupFilesEnd": 1779141118316, + "setupFilesStart": 1779141118316, + "slow": false, + "start": 1779141118316 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/service/report-analytics.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportAnalyticsService (integration)", + "getByReportId" + ], + "duration": 56, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService (integration) getByReportId returns analytics tied to the report when access is allowed", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141118391, + "status": "passed", + "title": "returns analytics tied to the report when access is allowed" + }, + { + "ancestorTitles": [ + "ReportAnalyticsService (integration)", + "getByReportId" + ], + "duration": 41, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService (integration) getByReportId returns an empty array when no analytics rows exist (access still checked)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118448, + "status": "passed", + "title": "returns an empty array when no analytics rows exist (access still checked)" + }, + { + "ancestorTitles": [ + "ReportAnalyticsService (integration)", + "getByReportId" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService (integration) getByReportId delegates access check to ReportingService (NotFound when report missing)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118489, + "status": "passed", + "title": "delegates access check to ReportingService (NotFound when report missing)" + }, + { + "ancestorTitles": [ + "ReportAnalyticsService (integration)", + "getByReportId" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService (integration) getByReportId delegates access check to ReportingService (Forbidden for unrelated actor)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118509, + "status": "passed", + "title": "delegates access check to ReportingService (Forbidden for unrelated actor)" + }, + { + "ancestorTitles": [ + "ReportAnalyticsService (integration)", + "getById" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService (integration) getById returns the analytics row when found and the actor has access", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118531, + "status": "passed", + "title": "returns the analytics row when found and the actor has access" + }, + { + "ancestorTitles": [ + "ReportAnalyticsService (integration)", + "getById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService (integration) getById throws NotFoundException when analytics id is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118551, + "status": "passed", + "title": "throws NotFoundException when analytics id is missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141118777, + "loadTestEnvironmentEnd": 1779141118573, + "loadTestEnvironmentStart": 1779141118571, + "runtime": 204, + "setupAfterEnvEnd": 1779141118591, + "setupAfterEnvStart": 1779141118580, + "setupFilesEnd": 1779141118573, + "setupFilesStart": 1779141118573, + "slow": false, + "start": 1779141118573 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/service/text-answer.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TextAnswerService (integration)", + "listByReview" + ], + "duration": 39, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService (integration) listByReview returns text answers with the question title mapped from relations (HR actor)", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141118636, + "status": "passed", + "title": "returns text answers with the question title mapped from relations (HR actor)" + }, + { + "ancestorTitles": [ + "TextAnswerService (integration)", + "listByReview" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService (integration) listByReview also allows ADMIN actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118675, + "status": "passed", + "title": "also allows ADMIN actors" + }, + { + "ancestorTitles": [ + "TextAnswerService (integration)", + "listByReview" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService (integration) listByReview allows callers without an actor (system caller)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118698, + "status": "passed", + "title": "allows callers without an actor (system caller)" + }, + { + "ancestorTitles": [ + "TextAnswerService (integration)", + "listByReview" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService (integration) listByReview throws ForbiddenException for a non-HR / non-Admin actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118719, + "status": "passed", + "title": "throws ForbiddenException for a non-HR / non-Admin actor" + }, + { + "ancestorTitles": [ + "TextAnswerService (integration)", + "listByReview" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService (integration) listByReview filters out numerical answers and answers with empty/whitespace text", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141118737, + "status": "passed", + "title": "filters out numerical answers and answers with empty/whitespace text" + }, + { + "ancestorTitles": [ + "TextAnswerService (integration)", + "listByReview" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService (integration) listByReview returns null for questionTitle when no relation exists for the question", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141118757, + "status": "passed", + "title": "returns null for questionTitle when no relation exists for the question" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141118968, + "loadTestEnvironmentEnd": 1779141118779, + "loadTestEnvironmentStart": 1779141118778, + "runtime": 189, + "setupAfterEnvEnd": 1779141118803, + "setupAfterEnvStart": 1779141118787, + "setupFilesEnd": 1779141118779, + "setupFilesStart": 1779141118779, + "slow": false, + "start": 1779141118779 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/review-question-relation.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewQuestionRelationRepository (integration)", + "link" + ], + "duration": 29, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationRepository (integration) link inserts a new relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141118861, + "status": "passed", + "title": "inserts a new relation" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationRepository (integration)", + "link" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationRepository (integration) link updates an existing relation on conflict (upsert)", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141118890, + "status": "passed", + "title": "updates an existing relation on conflict (upsert)" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationRepository (integration)", + "listByReview" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationRepository (integration) listByReview returns the relations attached to a review", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141118910, + "status": "passed", + "title": "returns the relations attached to a review" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationRepository (integration)", + "listByReview" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationRepository (integration) listByReview filters by questionTitle (case insensitive)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118929, + "status": "passed", + "title": "filters by questionTitle (case insensitive)" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationRepository (integration)", + "unlink" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationRepository (integration) unlink removes the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141118948, + "status": "passed", + "title": "removes the relation" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141119182, + "loadTestEnvironmentEnd": 1779141118969, + "loadTestEnvironmentStart": 1779141118968, + "runtime": 212, + "setupAfterEnvEnd": 1779141118988, + "setupAfterEnvStart": 1779141118977, + "setupFilesEnd": 1779141118970, + "setupFilesStart": 1779141118970, + "slow": false, + "start": 1779141118970 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/repository/report-analytics.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportAnalyticsRepository (integration)", + "createMany" + ], + "duration": 32, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsRepository (integration) createMany persists multiple analytics rows tied to a report", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119038, + "status": "passed", + "title": "persists multiple analytics rows tied to a report" + }, + { + "ancestorTitles": [ + "ReportAnalyticsRepository (integration)", + "createMany" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsRepository (integration) createMany is a no-op for an empty list", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119070, + "status": "passed", + "title": "is a no-op for an empty list" + }, + { + "ancestorTitles": [ + "ReportAnalyticsRepository (integration)", + "findByReportId" + ], + "duration": 22, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsRepository (integration) findByReportId returns analytics in ascending id order", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141119089, + "status": "passed", + "title": "returns analytics in ascending id order" + }, + { + "ancestorTitles": [ + "ReportAnalyticsRepository (integration)", + "findByReportId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsRepository (integration) findByReportId returns an empty array when no analytics exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119111, + "status": "passed", + "title": "returns an empty array when no analytics exist" + }, + { + "ancestorTitles": [ + "ReportAnalyticsRepository (integration)", + "findById" + ], + "duration": 32, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsRepository (integration) findById returns the analytics row when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119130, + "status": "passed", + "title": "returns the analytics row when found" + }, + { + "ancestorTitles": [ + "ReportAnalyticsRepository (integration)", + "findById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119162, + "status": "passed", + "title": "returns null for a missing id" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141119386, + "loadTestEnvironmentEnd": 1779141119183, + "loadTestEnvironmentStart": 1779141119182, + "runtime": 203, + "setupAfterEnvEnd": 1779141119203, + "setupAfterEnvStart": 1779141119191, + "setupFilesEnd": 1779141119183, + "setupFilesStart": 1779141119183, + "slow": false, + "start": 1779141119183 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/repository/report-comment.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportCommentRepository (integration)", + "create" + ], + "duration": 40, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentRepository (integration) create persists a comment row", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141119248, + "status": "passed", + "title": "persists a comment row" + }, + { + "ancestorTitles": [ + "ReportCommentRepository (integration)", + "create" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentRepository (integration) create persists a comment with no sentiment", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119288, + "status": "passed", + "title": "persists a comment with no sentiment" + }, + { + "ancestorTitles": [ + "ReportCommentRepository (integration)", + "findByReportId" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentRepository (integration) findByReportId returns comments in ascending id order", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119309, + "status": "passed", + "title": "returns comments in ascending id order" + }, + { + "ancestorTitles": [ + "ReportCommentRepository (integration)", + "findByReportId" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentRepository (integration) findByReportId returns an empty array when no comments exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119330, + "status": "passed", + "title": "returns an empty array when no comments exist" + }, + { + "ancestorTitles": [ + "ReportCommentRepository (integration)", + "findById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentRepository (integration) findById returns the comment when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119349, + "status": "passed", + "title": "returns the comment when found" + }, + { + "ancestorTitles": [ + "ReportCommentRepository (integration)", + "findById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119367, + "status": "passed", + "title": "returns null for a missing id" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 8, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141119593, + "loadTestEnvironmentEnd": 1779141119388, + "loadTestEnvironmentStart": 1779141119387, + "runtime": 205, + "setupAfterEnvEnd": 1779141119435, + "setupAfterEnvStart": 1779141119419, + "setupFilesEnd": 1779141119388, + "setupFilesStart": 1779141119388, + "slow": false, + "start": 1779141119388 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/organisation/repository/position.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionRepository (integration)", + "create" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionRepository (integration) create persists a position to the database", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141119478, + "status": "passed", + "title": "persists a position to the database" + }, + { + "ancestorTitles": [ + "PositionRepository (integration)", + "findById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionRepository (integration) findById returns the position when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119498, + "status": "passed", + "title": "returns the position when found" + }, + { + "ancestorTitles": [ + "PositionRepository (integration)", + "findById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119512, + "status": "passed", + "title": "returns null for a missing id" + }, + { + "ancestorTitles": [ + "PositionRepository (integration)", + "search" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionRepository (integration) search returns all positions when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119525, + "status": "passed", + "title": "returns all positions when no filter is supplied" + }, + { + "ancestorTitles": [ + "PositionRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionRepository (integration) search filters by title (case insensitive substring)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119538, + "status": "passed", + "title": "filters by title (case insensitive substring)" + }, + { + "ancestorTitles": [ + "PositionRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionRepository (integration) search honours descending sort direction on title", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119552, + "status": "passed", + "title": "honours descending sort direction on title" + }, + { + "ancestorTitles": [ + "PositionRepository (integration)", + "updateById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionRepository (integration) updateById persists the patched fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141119566, + "status": "passed", + "title": "persists the patched fields" + }, + { + "ancestorTitles": [ + "PositionRepository (integration)", + "deleteById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionRepository (integration) deleteById removes the position row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119579, + "status": "passed", + "title": "removes the position row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141119796, + "loadTestEnvironmentEnd": 1779141119595, + "loadTestEnvironmentStart": 1779141119594, + "runtime": 201, + "setupAfterEnvEnd": 1779141119616, + "setupAfterEnvStart": 1779141119603, + "setupFilesEnd": 1779141119595, + "setupFilesStart": 1779141119595, + "slow": false, + "start": 1779141119595 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/repository/report-insight.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportInsightRepository (integration)", + "createMany" + ], + "duration": 31, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightRepository (integration) createMany persists multiple insight rows tied to a report", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119667, + "status": "passed", + "title": "persists multiple insight rows tied to a report" + }, + { + "ancestorTitles": [ + "ReportInsightRepository (integration)", + "createMany" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightRepository (integration) createMany is a no-op for an empty list", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119698, + "status": "passed", + "title": "is a no-op for an empty list" + }, + { + "ancestorTitles": [ + "ReportInsightRepository (integration)", + "findByReportId" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightRepository (integration) findByReportId returns insights in ascending id order", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141119718, + "status": "passed", + "title": "returns insights in ascending id order" + }, + { + "ancestorTitles": [ + "ReportInsightRepository (integration)", + "findByReportId" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightRepository (integration) findByReportId returns an empty array when no insights exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119739, + "status": "passed", + "title": "returns an empty array when no insights exist" + }, + { + "ancestorTitles": [ + "ReportInsightRepository (integration)", + "findById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightRepository (integration) findById returns the insight when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119758, + "status": "passed", + "title": "returns the insight when found" + }, + { + "ancestorTitles": [ + "ReportInsightRepository (integration)", + "findById" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119777, + "status": "passed", + "title": "returns null for a missing id" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 8, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141119991, + "loadTestEnvironmentEnd": 1779141119798, + "loadTestEnvironmentStart": 1779141119797, + "runtime": 193, + "setupAfterEnvEnd": 1779141119818, + "setupAfterEnvStart": 1779141119806, + "setupFilesEnd": 1779141119798, + "setupFilesStart": 1779141119798, + "slow": false, + "start": 1779141119798 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/library/repository/cluster.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterRepository (integration)", + "create" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterRepository (integration) create persists a cluster row", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141119871, + "status": "passed", + "title": "persists a cluster row" + }, + { + "ancestorTitles": [ + "ClusterRepository (integration)", + "findById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterRepository (integration) findById returns the cluster when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119892, + "status": "passed", + "title": "returns the cluster when found" + }, + { + "ancestorTitles": [ + "ClusterRepository (integration)", + "findById" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterRepository (integration) findById returns null when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119906, + "status": "passed", + "title": "returns null when missing" + }, + { + "ancestorTitles": [ + "ClusterRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterRepository (integration) search returns all when no filter is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119919, + "status": "passed", + "title": "returns all when no filter is supplied" + }, + { + "ancestorTitles": [ + "ClusterRepository (integration)", + "search" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterRepository (integration) search filters by competenceId", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119933, + "status": "passed", + "title": "filters by competenceId" + }, + { + "ancestorTitles": [ + "ClusterRepository (integration)", + "search" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterRepository (integration) search honours descending sort on title", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119947, + "status": "passed", + "title": "honours descending sort on title" + }, + { + "ancestorTitles": [ + "ClusterRepository (integration)", + "updateById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterRepository (integration) updateById persists the patched fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119960, + "status": "passed", + "title": "persists the patched fields" + }, + { + "ancestorTitles": [ + "ClusterRepository (integration)", + "deleteById" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterRepository (integration) deleteById removes the cluster row", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141119976, + "status": "passed", + "title": "removes the cluster row" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141120168, + "loadTestEnvironmentEnd": 1779141119993, + "loadTestEnvironmentStart": 1779141119992, + "runtime": 175, + "setupAfterEnvEnd": 1779141120016, + "setupAfterEnvStart": 1779141120001, + "setupFilesEnd": 1779141119993, + "setupFilesStart": 1779141119993, + "slow": false, + "start": 1779141119993 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/review-stage-history.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageHistoryRepository (integration)", + "create" + ], + "duration": 27, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryRepository (integration) create persists a history row", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141120067, + "status": "passed", + "title": "persists a history row" + }, + { + "ancestorTitles": [ + "ReviewStageHistoryRepository (integration)", + "findByReviewId" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryRepository (integration) findByReviewId returns rows in ascending createdAt order", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120095, + "status": "passed", + "title": "returns rows in ascending createdAt order" + }, + { + "ancestorTitles": [ + "ReviewStageHistoryRepository (integration)", + "findByReviewId" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryRepository (integration) findByReviewId returns an empty array when no history exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120114, + "status": "passed", + "title": "returns an empty array when no history exists" + }, + { + "ancestorTitles": [ + "ReviewStageHistoryRepository (integration)", + "findById" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryRepository (integration) findById returns the row when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120131, + "status": "passed", + "title": "returns the row when found" + }, + { + "ancestorTitles": [ + "ReviewStageHistoryRepository (integration)", + "findById" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120150, + "status": "passed", + "title": "returns null for a missing id" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141120341, + "loadTestEnvironmentEnd": 1779141120170, + "loadTestEnvironmentStart": 1779141120168, + "runtime": 171, + "setupAfterEnvEnd": 1779141120189, + "setupAfterEnvStart": 1779141120178, + "setupFilesEnd": 1779141120170, + "setupFilesStart": 1779141120170, + "slow": false, + "start": 1779141120170 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/repository/cycle-stage-history.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageHistoryRepository (integration)", + "create" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryRepository (integration) create persists a new history row", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141120243, + "status": "passed", + "title": "persists a new history row" + }, + { + "ancestorTitles": [ + "CycleStageHistoryRepository (integration)", + "findByCycleId" + ], + "duration": 17, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryRepository (integration) findByCycleId returns rows in ascending createdAt order", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120267, + "status": "passed", + "title": "returns rows in ascending createdAt order" + }, + { + "ancestorTitles": [ + "CycleStageHistoryRepository (integration)", + "findByCycleId" + ], + "duration": 24, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryRepository (integration) findByCycleId returns an empty array when no history exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120284, + "status": "passed", + "title": "returns an empty array when no history exists" + }, + { + "ancestorTitles": [ + "CycleStageHistoryRepository (integration)", + "findById" + ], + "duration": 16, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryRepository (integration) findById returns the row when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120309, + "status": "passed", + "title": "returns the row when found" + }, + { + "ancestorTitles": [ + "CycleStageHistoryRepository (integration)", + "findById" + ], + "duration": 15, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryRepository (integration) findById returns null for a missing id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120325, + "status": "passed", + "title": "returns null for a missing id" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141120526, + "loadTestEnvironmentEnd": 1779141120343, + "loadTestEnvironmentStart": 1779141120342, + "runtime": 183, + "setupAfterEnvEnd": 1779141120375, + "setupAfterEnvStart": 1779141120355, + "setupFilesEnd": 1779141120343, + "setupFilesStart": 1779141120343, + "slow": false, + "start": 1779141120343 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/listeners/review-stage.listener.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageListener (feedback360, integration)", + "handleReviewStageChanged" + ], + "duration": 37, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (feedback360, integration) handleReviewStageChanged does nothing when toStage is not FINISHED and emits review.stage.processed", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141120422, + "status": "passed", + "title": "does nothing when toStage is not FINISHED and emits review.stage.processed" + }, + { + "ancestorTitles": [ + "ReviewStageListener (feedback360, integration)", + "handleReviewStageChanged" + ], + "duration": 26, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (feedback360, integration) handleReviewStageChanged completes the cycle when this is the last active review in it", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120459, + "status": "passed", + "title": "completes the cycle when this is the last active review in it" + }, + { + "ancestorTitles": [ + "ReviewStageListener (feedback360, integration)", + "handleReviewStageChanged" + ], + "duration": 21, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (feedback360, integration) handleReviewStageChanged does not complete the cycle when other reviews are still in progress", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120485, + "status": "passed", + "title": "does not complete the cycle when other reviews are still in progress" + }, + { + "ancestorTitles": [ + "ReviewStageListener (feedback360, integration)", + "handleReviewStageChanged" + ], + "duration": 19, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (feedback360, integration) handleReviewStageChanged still emits review.stage.processed when the review has no cycle attached", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141120506, + "status": "passed", + "title": "still emits review.stage.processed when the review has no cycle attached" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141120692, + "loadTestEnvironmentEnd": 1779141120527, + "loadTestEnvironmentStart": 1779141120526, + "runtime": 165, + "setupAfterEnvEnd": 1779141120547, + "setupAfterEnvStart": 1779141120535, + "setupFilesEnd": 1779141120527, + "setupFilesStart": 1779141120527, + "slow": false, + "start": 1779141120527 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/listeners/cycle-stage.listener.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageListener (reporting, integration)", + "handleCycleStageProcessed" + ], + "duration": 32, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener (reporting, integration) handleCycleStageProcessed does nothing when currentStage is not FINISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141120610, + "status": "passed", + "title": "does nothing when currentStage is not FINISHED" + }, + { + "ancestorTitles": [ + "CycleStageListener (reporting, integration)", + "handleCycleStageProcessed" + ], + "duration": 49, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener (reporting, integration) handleCycleStageProcessed drives the cycle FINISHED → PREPARING_REPORT → PUBLISHED and generates a strategic report", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141120642, + "status": "passed", + "title": "drives the cycle FINISHED → PREPARING_REPORT → PUBLISHED and generates a strategic report" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141120846, + "loadTestEnvironmentEnd": 1779141120694, + "loadTestEnvironmentStart": 1779141120692, + "runtime": 152, + "setupAfterEnvEnd": 1779141120713, + "setupAfterEnvStart": 1779141120701, + "setupFilesEnd": 1779141120694, + "setupFilesStart": 1779141120694, + "slow": false, + "start": 1779141120694 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/feedback360/listeners/cycle-stage.listener.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageListener (feedback360, integration)", + "handleCycleStageChanged" + ], + "duration": 30, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener (feedback360, integration) handleCycleStageChanged emits cycle.stage.processed and does nothing else when toStage is not FINISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141120767, + "status": "passed", + "title": "emits cycle.stage.processed and does nothing else when toStage is not FINISHED" + }, + { + "ancestorTitles": [ + "CycleStageListener (feedback360, integration)", + "handleCycleStageChanged" + ], + "duration": 20, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener (feedback360, integration) handleCycleStageChanged emits cycle.stage.processed but skips analytics when the cycle has no reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141120798, + "status": "passed", + "title": "emits cycle.stage.processed but skips analytics when the cycle has no reviews" + }, + { + "ancestorTitles": [ + "CycleStageListener (feedback360, integration)", + "handleCycleStageChanged" + ], + "duration": 27, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener (feedback360, integration) handleCycleStageChanged generates analytics when the cycle has reviews + cluster scores and emits processed", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141120818, + "status": "passed", + "title": "generates analytics when the cycle has reviews + cluster scores and emits processed" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141121105, + "loadTestEnvironmentEnd": 1779141120847, + "loadTestEnvironmentStart": 1779141120846, + "runtime": 258, + "setupAfterEnvEnd": 1779141120866, + "setupAfterEnvStart": 1779141120855, + "setupFilesEnd": 1779141120847, + "setupFilesStart": 1779141120847, + "slow": false, + "start": 1779141120847 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/reporting/listeners/review-stage.listener.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageListener (reporting, integration)", + "handleReviewStageProcessed" + ], + "duration": 46, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (reporting, integration) handleReviewStageProcessed does nothing when currentStage is not FINISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141121017, + "status": "passed", + "title": "does nothing when currentStage is not FINISHED" + }, + { + "ancestorTitles": [ + "ReviewStageListener (reporting, integration)", + "handleReviewStageProcessed" + ], + "duration": 41, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (reporting, integration) handleReviewStageProcessed drives review FINISHED → PREPARING_REPORT → PROCESSING_BY_HR and generates an individual report", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141121063, + "status": "passed", + "title": "drives review FINISHED → PREPARING_REPORT → PROCESSING_BY_HR and generates an individual report" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141121188, + "loadTestEnvironmentEnd": 1779141121108, + "loadTestEnvironmentStart": 1779141121106, + "runtime": 80, + "setupAfterEnvEnd": 1779141121131, + "setupAfterEnvStart": 1779141121117, + "setupFilesEnd": 1779141121108, + "setupFilesStart": 1779141121108, + "slow": false, + "start": 1779141121108 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/identity/repository/role.repository.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RoleRepository (integration)", + "getAll" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleRepository (integration) getAll returns all four seeded identity roles", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141121184, + "status": "passed", + "title": "returns all four seeded identity roles" + }, + { + "ancestorTitles": [ + "RoleRepository (integration)", + "getAll" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleRepository (integration) getAll returns roles in a stable order across calls", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141121185, + "status": "passed", + "title": "returns roles in a stable order across calls" + }, + { + "ancestorTitles": [ + "RoleRepository (integration)", + "findByCodes" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleRepository (integration) findByCodes returns the roles matching the requested codes", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141121186, + "status": "passed", + "title": "returns the roles matching the requested codes" + }, + { + "ancestorTitles": [ + "RoleRepository (integration)", + "findByCodes" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleRepository (integration) findByCodes returns an empty array for an empty codes input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141121186, + "status": "passed", + "title": "returns an empty array for an empty codes input" + }, + { + "ancestorTitles": [ + "RoleRepository (integration)", + "findByCodes" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleRepository (integration) findByCodes returns only roles that actually exist when subset is requested", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141121187, + "status": "passed", + "title": "returns only roles that actually exist when subset is requested" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141121285, + "loadTestEnvironmentEnd": 1779141121190, + "loadTestEnvironmentStart": 1779141121189, + "runtime": 95, + "setupAfterEnvEnd": 1779141121209, + "setupAfterEnvStart": 1779141121198, + "setupFilesEnd": 1779141121190, + "setupFilesStart": 1779141121190, + "slow": false, + "start": 1779141121190 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/integration/contexts/identity/service/identity-role.service.integration-spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "IdentityRoleService (integration)", + "list" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityRoleService (integration) list returns every seeded identity role from the database", + "invocations": 1, + "location": null, + "numPassingAsserts": 13, + "retryReasons": [], + "startAt": 1779141121280, + "status": "passed", + "title": "returns every seeded identity role from the database" + }, + { + "ancestorTitles": [ + "IdentityRoleService (integration)", + "list" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityRoleService (integration) list returns roles in a deterministic order across calls", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141121282, + "status": "passed", + "title": "returns roles in a deterministic order across calls" + }, + { + "ancestorTitles": [ + "IdentityRoleService (integration)", + "getByCodes" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityRoleService (integration) getByCodes returns only the roles whose codes are requested", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141121283, + "status": "passed", + "title": "returns only the roles whose codes are requested" + }, + { + "ancestorTitles": [ + "IdentityRoleService (integration)", + "getByCodes" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityRoleService (integration) getByCodes returns an empty array for an empty codes input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141121284, + "status": "passed", + "title": "returns an empty array for an empty codes input" + }, + { + "ancestorTitles": [ + "IdentityRoleService (integration)", + "getByCodes" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityRoleService (integration) getByCodes returns whatever subset of requested codes actually exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141121284, + "status": "passed", + "title": "returns whatever subset of requested codes actually exists" + } + ], + "failureMessage": null + } + ], + "wasInterrupted": false +} \ No newline at end of file diff --git a/apps/api/test/integration/setup-env.ts b/apps/api/test/integration/setup-env.ts new file mode 100644 index 00000000..87f47b48 --- /dev/null +++ b/apps/api/test/integration/setup-env.ts @@ -0,0 +1,151 @@ +import * as dotenv from 'dotenv'; +import { existsSync } from 'fs'; +import { resolve } from 'path'; +import { Client } from 'pg'; + +const envFile = resolve(__dirname, '../../../../.env.test'); + +if (!existsSync(envFile)) { + throw new Error( + `Integration tests require ${envFile} with DATABASE_* credentials.`, + ); +} + +// `quiet: true` suppresses dotenv 17.x's "tip" log line that otherwise +// shows up in every spec via the Jest console reporter. +dotenv.config({ path: envFile, quiet: true }); + +// Fallback: when this file is loaded directly by Jest (not through +// `dotenv-cli -e ...`) the `${DATABASE_USER}`/`${DATABASE_PASSWORD}`/... +// placeholders inside DATABASE_URL are not expanded by `dotenv` itself. +// Re-build the URL from the individual variables in that case so the +// integration suite works either with or without the dotenv-cli wrapper. +if (process.env.DATABASE_URL?.includes('${DATABASE_')) { + const { + DATABASE_USER, + DATABASE_PASSWORD, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_NAME, + } = process.env; + + if ( + DATABASE_USER && + DATABASE_PASSWORD && + DATABASE_HOST && + DATABASE_PORT && + DATABASE_NAME + ) { + process.env.DATABASE_URL = `postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}`; + } +} + +/** + * Auto-creates the test database in the configured Postgres instance if + * it doesn't exist yet. Idempotent and cached per process — multiple + * spec files can call it freely; only the first call actually talks to + * Postgres. + * + * Why: integration tests live in their own DB (e.g. `intra_test`) so + * they can TRUNCATE freely without touching dev data. Asking developers + * to run `CREATE DATABASE intra_test` by hand is friction; doing it + * inline keeps the workflow to "spin up Postgres → run tests". + * + * The schema itself is NOT created here — run `pnpm db:test:refresh` + * (or `pnpm test:integration:refresh`) once to apply Prisma migrations + * after the database is created. Subsequent test runs only need + * `pnpm test:integration`. + */ +let dbReadyPromise: Promise | null = null; + +export function ensureTestDatabaseExists(): Promise { + if (dbReadyPromise) return dbReadyPromise; + + dbReadyPromise = (async () => { + const { + DATABASE_USER, + DATABASE_PASSWORD, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_NAME, + } = process.env; + + if ( + !DATABASE_USER || + !DATABASE_PASSWORD || + !DATABASE_HOST || + !DATABASE_PORT || + !DATABASE_NAME + ) { + throw new Error( + 'DATABASE_USER/PASSWORD/HOST/PORT/NAME must be set ' + + 'for integration tests (see .env.test).', + ); + } + + // Connect to the `postgres` admin DB to inspect / create the + // target database — you cannot `CREATE DATABASE` while + // connected to the database being created. + const admin = new Client({ + host: DATABASE_HOST, + port: Number(DATABASE_PORT), + user: DATABASE_USER, + password: DATABASE_PASSWORD, + database: 'postgres', + }); + + try { + await admin.connect(); + } catch (error) { + throw new Error( + `Failed to reach Postgres at ${DATABASE_HOST}:${DATABASE_PORT} ` + + `as user "${DATABASE_USER}". Is Docker running? ` + + `(\`pnpm docker:up\`)\nUnderlying error: ${ + (error as Error).message + }`, + ); + } + + try { + const { rows } = await admin.query( + `SELECT 1 FROM pg_database WHERE datname = $1`, + [DATABASE_NAME], + ); + + if (rows.length === 0) { + // CREATE DATABASE doesn't accept parameters — quote the + // identifier instead. The name comes from .env.test, which + // we control, so injection isn't a concern here. + await admin.query(`CREATE DATABASE "${DATABASE_NAME}"`); + // eslint-disable-next-line no-console + console.log( + `[integration] Created test database "${DATABASE_NAME}"`, + ); + } + } finally { + await admin.end(); + } + })(); + + return dbReadyPromise; +} + +if (process.env.DATABASE_URL?.includes('${DATABASE_')) { + const { + DATABASE_USER, + DATABASE_PASSWORD, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_NAME, + } = process.env; + + if ( + DATABASE_USER && + DATABASE_PASSWORD && + DATABASE_HOST && + DATABASE_PORT && + DATABASE_NAME + ) { + process.env.DATABASE_URL = `postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}`; + } +} diff --git a/apps/api/test/integration/setup-jest.ts b/apps/api/test/integration/setup-jest.ts new file mode 100644 index 00000000..1999d554 --- /dev/null +++ b/apps/api/test/integration/setup-jest.ts @@ -0,0 +1,24 @@ +/** + * Jest setup file for the integration suite. + * + * Silences NestJS Logger output during the run. The integration tests + * deliberately exercise paths that log errors: async listeners that + * fire after a `resetXxxTables()` truncated the database in the next + * `beforeEach`, the MailerStub throwing on demand to drive the failure + * branch, and so on. These errors are caught by their try/catch blocks + * — the tests still pass — but the raw Nest log output drowns the + * actual Jest result summary. + * + * Errors that genuinely fail a test still surface through Jest's own + * reporter (expect rejections, thrown assertions, etc.), so silencing + * the Logger here does not hide real failures. + */ +import { Logger } from '@nestjs/common'; + +const noop = () => undefined; + +jest.spyOn(Logger.prototype, 'error').mockImplementation(noop); +jest.spyOn(Logger.prototype, 'warn').mockImplementation(noop); +jest.spyOn(Logger.prototype, 'log').mockImplementation(noop); +jest.spyOn(Logger.prototype, 'debug').mockImplementation(noop); +jest.spyOn(Logger.prototype, 'verbose').mockImplementation(noop); diff --git a/apps/api/test/load/docker-compose.k6.yml b/apps/api/test/load/docker-compose.k6.yml new file mode 100644 index 00000000..5a444c63 --- /dev/null +++ b/apps/api/test/load/docker-compose.k6.yml @@ -0,0 +1,21 @@ +services: + k6: + image: grafana/k6:latest + container_name: intra-k6 + network_mode: bridge + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + BASE_URL: "${K6_BASE_URL:-http://host.docker.internal:8080}" + DEV_LOGIN_EMAIL: "${K6_DEV_LOGIN_EMAIL:-oleksandr.bondarenko@intra.com}" + K6_WEB_DASHBOARD: "true" + K6_WEB_DASHBOARD_EXPORT: "/results/${K6_REPORT_NAME:-report}.html" + K6_SUMMARY_EXPORT: "/results/${K6_REPORT_NAME:-report}.summary.json" + volumes: + - ./scripts:/scripts:ro + - ./results:/results + ulimits: + nofile: + soft: 1048576 + hard: 1048576 + entrypoint: ["k6"] diff --git a/apps/api/test/load/results/.gitkeep b/apps/api/test/load/results/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/test/load/scripts/lib/auth.js b/apps/api/test/load/scripts/lib/auth.js new file mode 100644 index 00000000..5f4a01b7 --- /dev/null +++ b/apps/api/test/load/scripts/lib/auth.js @@ -0,0 +1,28 @@ +import http from 'k6/http'; +import { check, fail } from 'k6'; +import { BASE_URL, DEV_LOGIN_EMAIL } from './config.js'; + +export function login(email = DEV_LOGIN_EMAIL) { + const res = http.post( + `${BASE_URL}/auth/dev/login`, + JSON.stringify({ email }), + { headers: { 'Content-Type': 'application/json' }, tags: { name: 'auth/dev/login' } }, + ); + + const ok = check(res, { 'login 2xx': (r) => r.status >= 200 && r.status < 300 }); + if (!ok) fail(`dev-login failed for ${email}: ${res.status} ${res.body}`); + + let token; + try { + token = res.json('session.token'); + } catch (_) { + token = null; + } + if (!token) fail(`No session.token in login response: ${res.body}`); + + return token; +} + +export function authHeaders(token) { + return { Authorization: `Bearer ${token}` }; +} diff --git a/apps/api/test/load/scripts/lib/config.js b/apps/api/test/load/scripts/lib/config.js new file mode 100644 index 00000000..59ef0021 --- /dev/null +++ b/apps/api/test/load/scripts/lib/config.js @@ -0,0 +1,19 @@ +export const BASE_URL = __ENV.BASE_URL || 'http://host.docker.internal:8080'; +export const DEV_LOGIN_EMAIL = __ENV.DEV_LOGIN_EMAIL || 'oleksandr.bondarenko@intra.com'; + +export const SLO = { + INTERACTIVE_P95_MS: 500, + PDF_BUDGET_MS: 15000, + ERROR_RATE: 0.01, +}; + +export const SEED_EMAILS = [ + 'oleksandr.bondarenko@intra.com', + 'dmytro.kovalenko@intra.com', + 'iryna.shevchenko@intra.com', + 'olena.boyko@intra.com', + 'andrii.melnyk@intra.com', + 'natalya.tkachenko@intra.com', + 'mariia.pavlenko@intra.com', + 'yulia.kravchenko@intra.com', +]; diff --git a/apps/api/test/load/scripts/lib/endpoints.js b/apps/api/test/load/scripts/lib/endpoints.js new file mode 100644 index 00000000..c636e719 --- /dev/null +++ b/apps/api/test/load/scripts/lib/endpoints.js @@ -0,0 +1,18 @@ +export const INTERACTIVE_ENDPOINTS = [ + { method: 'GET', path: '/auth/me', weight: 5, name: 'auth/me' }, + { method: 'GET', path: '/identity/users?limit=20', weight: 4, name: 'identity/users:search' }, + { method: 'GET', path: '/identity/roles', weight: 1, name: 'identity/roles:list' }, + { method: 'GET', path: '/organisation/teams?limit=20', weight: 3, name: 'organisation/teams:search' }, + { method: 'GET', path: '/organisation/positions?limit=20', weight: 2, name: 'organisation/positions:search' }, + { method: 'GET', path: '/reporting/reports?limit=20', weight: 3, name: 'reporting/reports:search' }, +]; + +export function pickWeighted(items) { + const total = items.reduce((s, e) => s + (e.weight || 1), 0); + let r = Math.random() * total; + for (const e of items) { + r -= e.weight || 1; + if (r <= 0) return e; + } + return items[items.length - 1]; +} diff --git a/apps/api/test/load/scripts/scenarios/baseline-p95.js b/apps/api/test/load/scripts/scenarios/baseline-p95.js new file mode 100644 index 00000000..7b100da4 --- /dev/null +++ b/apps/api/test/load/scripts/scenarios/baseline-p95.js @@ -0,0 +1,41 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { BASE_URL, SLO } from '../lib/config.js'; +import { login, authHeaders } from '../lib/auth.js'; +import { INTERACTIVE_ENDPOINTS, pickWeighted } from '../lib/endpoints.js'; + +export const options = { + scenarios: { + baseline: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '1m', target: 50 }, + { duration: '5m', target: 50 }, + { duration: '30s', target: 0 }, + ], + gracefulRampDown: '30s', + }, + }, + thresholds: { + http_req_failed: [`rate<${SLO.ERROR_RATE}`], + 'http_req_duration{kind:interactive}': [ + `p(95)<${SLO.INTERACTIVE_P95_MS}`, + `p(99)<${SLO.INTERACTIVE_P95_MS * 2}`, + ], + }, +}; + +export function setup() { + return { token: login() }; +} + +export default function (data) { + const ep = pickWeighted(INTERACTIVE_ENDPOINTS); + const res = http.request(ep.method, `${BASE_URL}${ep.path}`, null, { + headers: authHeaders(data.token), + tags: { name: ep.name, kind: 'interactive' }, + }); + check(res, { 'status 2xx': (r) => r.status >= 200 && r.status < 300 }); + sleep(Math.random() * 2 + 0.5); +} diff --git a/apps/api/test/load/scripts/scenarios/load-500vu.js b/apps/api/test/load/scripts/scenarios/load-500vu.js new file mode 100644 index 00000000..93ec64f7 --- /dev/null +++ b/apps/api/test/load/scripts/scenarios/load-500vu.js @@ -0,0 +1,42 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { BASE_URL, SLO, SEED_EMAILS } from '../lib/config.js'; +import { login, authHeaders } from '../lib/auth.js'; +import { INTERACTIVE_ENDPOINTS, pickWeighted } from '../lib/endpoints.js'; + +export const options = { + scenarios: { + load_500_sessions: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '2m', target: 100 }, + { duration: '2m', target: 300 }, + { duration: '2m', target: 500 }, + { duration: '10m', target: 500 }, + { duration: '1m', target: 0 }, + ], + gracefulRampDown: '30s', + }, + }, + thresholds: { + http_req_failed: [`rate<${SLO.ERROR_RATE}`], + 'http_req_duration{kind:interactive}': [`p(95)<${SLO.INTERACTIVE_P95_MS}`], + }, +}; + +export function setup() { + const tokens = SEED_EMAILS.map((email) => login(email)); + return { tokens }; +} + +export default function (data) { + const token = data.tokens[__VU % data.tokens.length]; + const ep = pickWeighted(INTERACTIVE_ENDPOINTS); + const res = http.request(ep.method, `${BASE_URL}${ep.path}`, null, { + headers: authHeaders(token), + tags: { name: ep.name, kind: 'interactive' }, + }); + check(res, { 'status 2xx': (r) => r.status >= 200 && r.status < 300 }); + sleep(Math.random() * 2 + 0.5); +} diff --git a/apps/api/test/load/scripts/scenarios/pdf-15s.js b/apps/api/test/load/scripts/scenarios/pdf-15s.js new file mode 100644 index 00000000..1b382090 --- /dev/null +++ b/apps/api/test/load/scripts/scenarios/pdf-15s.js @@ -0,0 +1,83 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { BASE_URL, SLO } from '../lib/config.js'; +import { login, authHeaders } from '../lib/auth.js'; + +const PDF_GENERATE_PATH = __ENV.PDF_GENERATE_PATH || '/reporting/reports/{id}/pdf'; +const PDF_POLL_PATH = __ENV.PDF_POLL_PATH || '/reporting/reports/jobs/{jobId}'; +const REPORT_ID = __ENV.REPORT_ID || ''; +const SYNC_MODE = (__ENV.PDF_SYNC_MODE || 'false') === 'true'; + +export const options = { + scenarios: { + pdf_generation: { + executor: 'per-vu-iterations', + vus: 5, + iterations: 5, + maxDuration: '10m', + }, + }, + thresholds: { + 'pdf_total_ms': [`p(95)<${SLO.PDF_BUDGET_MS}`], + http_req_failed: ['rate<0.02'], + }, +}; + +import { Trend } from 'k6/metrics'; +const pdfTotalMs = new Trend('pdf_total_ms'); + +export function setup() { + if (!REPORT_ID) { + throw new Error('Set REPORT_ID env var (id of an existing report)'); + } + return { token: login(), reportId: REPORT_ID }; +} + +export default function (data) { + const headers = authHeaders(data.token); + const startUrl = `${BASE_URL}${PDF_GENERATE_PATH.replace('{id}', data.reportId)}`; + const t0 = Date.now(); + + const startRes = http.post(startUrl, null, { headers, tags: { name: 'pdf:start' } }); + + if (SYNC_MODE) { + const elapsed = Date.now() - t0; + pdfTotalMs.add(elapsed); + check(startRes, { + 'pdf status 2xx': (r) => r.status >= 200 && r.status < 300, + 'pdf < 15s': () => elapsed < SLO.PDF_BUDGET_MS, + }); + return; + } + + if (!check(startRes, { 'start accepted': (r) => r.status === 202 || r.status === 200 })) { + return; + } + + let jobId; + try { + jobId = startRes.json('jobId') || startRes.json('id'); + } catch (_) { + return; + } + if (!jobId) return; + + const pollUrl = `${BASE_URL}${PDF_POLL_PATH.replace('{jobId}', jobId)}`; + while (Date.now() - t0 < SLO.PDF_BUDGET_MS + 5000) { + sleep(0.5); + const pollRes = http.get(pollUrl, { headers, tags: { name: 'pdf:poll' } }); + let status; + try { status = pollRes.json('status'); } catch (_) { break; } + if (status === 'done' || status === 'completed' || status === 'success') { + const elapsed = Date.now() - t0; + pdfTotalMs.add(elapsed); + check(null, { 'pdf < 15s': () => elapsed < SLO.PDF_BUDGET_MS }); + return; + } + if (status === 'failed' || status === 'error') { + check(null, { 'pdf job did not fail': () => false }); + return; + } + } + check(null, { 'pdf finished in time': () => false }); +} diff --git a/apps/api/test/load/scripts/scenarios/smoke.js b/apps/api/test/load/scripts/scenarios/smoke.js new file mode 100644 index 00000000..a6dadb2f --- /dev/null +++ b/apps/api/test/load/scripts/scenarios/smoke.js @@ -0,0 +1,33 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { BASE_URL, SLO } from '../lib/config.js'; +import { login, authHeaders } from '../lib/auth.js'; +import { INTERACTIVE_ENDPOINTS, pickWeighted } from '../lib/endpoints.js'; + +export const options = { + scenarios: { + smoke: { + executor: 'constant-vus', + vus: 1, + duration: '30s', + }, + }, + thresholds: { + http_req_failed: [`rate<${SLO.ERROR_RATE}`], + 'http_req_duration{kind:interactive}': [`p(95)<${SLO.INTERACTIVE_P95_MS}`], + }, +}; + +export function setup() { + return { token: login() }; +} + +export default function (data) { + const ep = pickWeighted(INTERACTIVE_ENDPOINTS); + const res = http.request(ep.method, `${BASE_URL}${ep.path}`, null, { + headers: authHeaders(data.token), + tags: { name: ep.name, kind: 'interactive' }, + }); + check(res, { 'status 2xx': (r) => r.status >= 200 && r.status < 300 }); + sleep(1); +} diff --git a/apps/api/test/load/scripts/scenarios/stress-1000vu.js b/apps/api/test/load/scripts/scenarios/stress-1000vu.js new file mode 100644 index 00000000..83ce7546 --- /dev/null +++ b/apps/api/test/load/scripts/scenarios/stress-1000vu.js @@ -0,0 +1,45 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { BASE_URL, SLO, SEED_EMAILS } from '../lib/config.js'; +import { login, authHeaders } from '../lib/auth.js'; +import { INTERACTIVE_ENDPOINTS, pickWeighted } from '../lib/endpoints.js'; + +export const options = { + scenarios: { + stress_to_breaking_point: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '2m', target: 250 }, + { duration: '2m', target: 500 }, + { duration: '3m', target: 1000 }, + { duration: '5m', target: 1000 }, + { duration: '2m', target: 1500 }, + { duration: '3m', target: 1500 }, + { duration: '1m', target: 0 }, + ], + gracefulRampDown: '30s', + }, + }, + thresholds: { + http_req_failed: ['rate<0.05'], + 'http_req_duration{kind:interactive}': [ + { threshold: `p(95)<${SLO.INTERACTIVE_P95_MS}`, abortOnFail: false }, + ], + }, +}; + +export function setup() { + return { tokens: SEED_EMAILS.map((e) => login(e)) }; +} + +export default function (data) { + const token = data.tokens[__VU % data.tokens.length]; + const ep = pickWeighted(INTERACTIVE_ENDPOINTS); + const res = http.request(ep.method, `${BASE_URL}${ep.path}`, null, { + headers: authHeaders(token), + tags: { name: ep.name, kind: 'interactive' }, + }); + check(res, { 'status 2xx': (r) => r.status >= 200 && r.status < 300 }); + sleep(Math.random() * 1 + 0.3); +} diff --git a/apps/api/test/scripts/generate-test-dashboard.ts b/apps/api/test/scripts/generate-test-dashboard.ts new file mode 100644 index 00000000..8dcae795 --- /dev/null +++ b/apps/api/test/scripts/generate-test-dashboard.ts @@ -0,0 +1,597 @@ +/* eslint-disable */ +/** + * Generates an HTML dashboard from Jest's AggregatedResult JSON. + * + * Usage: + * tsx generate-test-dashboard.ts + * + * The script aggregates results per bounded context (identity, + * organisation, library, feedback360, reporting, notifications) by + * walking each spec file path and pulling the segment that comes + * after `contexts/`. Anything outside `contexts/` is bucketed under + * "other". + * + * The rendered page uses Chart.js via CDN and stays self-contained + * (no JSON fetches, the data is inlined into the HTML). + */ +import { readFileSync, writeFileSync } from 'fs'; +import { resolve } from 'path'; + +type JestTestResult = { + testFilePath: string; + numFailingTests: number; + numPassingTests: number; + numPendingTests: number; + numTodoTests: number; + perfStats: { runtime: number }; + testResults: Array<{ status: string }>; +}; + +type JestAggregatedResult = { + /** + * Jest's own `success` flag is unreliable when serialised from + * `onRunComplete` of a custom reporter — it's the intermediate + * value, not the final one Jest computes AFTER reporters run. + * The dashboard derives a trustworthy `success` from the numeric + * fields below (see `isRunSuccessful`). + */ + success: boolean; + numTotalTestSuites: number; + numPassedTestSuites: number; + numFailedTestSuites: number; + numPendingTestSuites: number; + numRuntimeErrorTestSuites?: number; + numTotalTests: number; + numPassedTests: number; + numFailedTests: number; + numPendingTests: number; + numTodoTests: number; + startTime: number; + testResults: JestTestResult[]; +}; + +function isRunSuccessful(results: JestAggregatedResult): boolean { + return ( + results.numFailedTests === 0 && + results.numFailedTestSuites === 0 && + (results.numRuntimeErrorTestSuites ?? 0) === 0 + ); +} + +type ContextStats = { + name: string; + suites: number; + passedSuites: number; + failedSuites: number; + tests: number; + passed: number; + failed: number; + pending: number; + durationMs: number; +}; + +const CONTEXT_ORDER = [ + 'identity', + 'organisation', + 'library', + 'feedback360', + 'reporting', + 'notifications', +]; + +// Soft palette tuned for projection — high enough contrast on white, +// no harsh saturated red/green clashes. +const CONTEXT_COLORS: Record = { + identity: '#6366f1', + organisation: '#06b6d4', + library: '#10b981', + feedback360: '#f59e0b', + reporting: '#8b5cf6', + notifications: '#ec4899', + other: '#94a3b8', +}; + +function extractContext(filePath: string): string { + const match = filePath.match(/\/contexts\/([^\/]+)\//); + return match ? match[1] : 'other'; +} + +function aggregate(results: JestAggregatedResult): ContextStats[] { + const map = new Map(); + for (const r of results.testResults) { + const ctx = extractContext(r.testFilePath); + const stats = + map.get(ctx) ?? + ({ + name: ctx, + suites: 0, + passedSuites: 0, + failedSuites: 0, + tests: 0, + passed: 0, + failed: 0, + pending: 0, + durationMs: 0, + } as ContextStats); + stats.suites += 1; + const suiteFailed = r.numFailingTests > 0; + if (suiteFailed) stats.failedSuites += 1; + else stats.passedSuites += 1; + stats.tests += + r.numPassingTests + r.numFailingTests + r.numPendingTests; + stats.passed += r.numPassingTests; + stats.failed += r.numFailingTests; + stats.pending += r.numPendingTests; + stats.durationMs += r.perfStats?.runtime ?? 0; + map.set(ctx, stats); + } + return [...map.values()].sort((a, b) => { + const ia = CONTEXT_ORDER.indexOf(a.name); + const ib = CONTEXT_ORDER.indexOf(b.name); + if (ia === -1 && ib === -1) return a.name.localeCompare(b.name); + if (ia === -1) return 1; + if (ib === -1) return -1; + return ia - ib; + }); +} + +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms} ms`; + const seconds = ms / 1000; + if (seconds < 60) return `${seconds.toFixed(2)} s`; + const minutes = Math.floor(seconds / 60); + const restSec = (seconds - minutes * 60).toFixed(1); + return `${minutes}m ${restSec}s`; +} + +function renderHtml( + title: string, + suiteLabel: string, + results: JestAggregatedResult, + stats: ContextStats[], +): string { + const totalDuration = stats.reduce((acc, s) => acc + s.durationMs, 0); + const passRate = results.numTotalTests + ? ((results.numPassedTests / results.numTotalTests) * 100).toFixed(1) + : '0.0'; + const generatedAt = new Date().toLocaleString('uk-UA', { + dateStyle: 'long', + timeStyle: 'short', + }); + const labels = stats.map((s) => s.name); + const colors = stats.map((s) => CONTEXT_COLORS[s.name] ?? '#64748b'); + + const data = { + labels, + colors, + suites: stats.map((s) => s.suites), + passed: stats.map((s) => s.passed), + failed: stats.map((s) => s.failed), + durations: stats.map((s) => Math.round(s.durationMs)), + }; + + return ` + + + + + ${title} + + + + + +
+

+ ${title} + ${isRunSuccessful(results) ? 'PASS' : 'FAIL'} +

+
+ ${suiteLabel} · згенеровано ${generatedAt} +
+
+ +
+
+
Test suites
+
${results.numTotalTestSuites}
+
+ ${results.numPassedTestSuites} passed${ + results.numFailedTestSuites + ? ` · ${results.numFailedTestSuites} failed` + : '' + } +
+
+
+
Tests
+
${results.numTotalTests}
+
+ ${results.numPassedTests} passed${ + results.numFailedTests + ? ` · ${results.numFailedTests} failed` + : '' + }${ + results.numPendingTests + ? ` · ${results.numPendingTests} pending` + : '' + } +
+
+
+
Pass rate
+
${passRate}%
+
${results.numPassedTests}/${results.numTotalTests} tests
+
+
+
Duration
+
${formatDuration(totalDuration)}
+
cumulative across suites
+
+
+ +
+
+

Tests per context

+
+
+
+

Distribution by context

+
+
+
+ +
+
+

Suites per context

+
+
+
+

Duration per context

+
+
+
+ + + + + + + + + + + + + + ${stats + .map( + (s) => ` + + + + + + + `, + ) + .join('\n')} + +
ContextSuitesTestsPassedFailedDuration
+ ${s.name} + ${s.suites}${s.tests}${s.passed}${s.failed}${formatDuration(s.durationMs)}
+ +
+ Згенеровано із Jest AggregatedResult · + ${results.testResults.length} spec files +
+ + + +`; +} + +function main(): void { + const [, , inputArg, outputArg, ...rest] = process.argv; + if (!inputArg || !outputArg) { + console.error( + 'Usage: tsx generate-test-dashboard.ts [suite-label]', + ); + process.exit(1); + } + const inputPath = resolve(inputArg); + const outputPath = resolve(outputArg); + const suiteLabel = rest.join(' ') || 'Test run'; + const title = suiteLabel.includes('Unit') + ? 'Unit Tests Dashboard' + : suiteLabel.includes('Integration') + ? 'Integration Tests Dashboard' + : 'Tests Dashboard'; + + const raw = readFileSync(inputPath, 'utf8'); + const results = JSON.parse(raw) as JestAggregatedResult; + const stats = aggregate(results); + const html = renderHtml(title, suiteLabel, results, stats); + writeFileSync(outputPath, html, 'utf8'); + console.log(`Dashboard written to ${outputPath}`); +} + +main(); diff --git a/apps/api/test/scripts/json-results.reporter.js b/apps/api/test/scripts/json-results.reporter.js new file mode 100644 index 00000000..65193684 --- /dev/null +++ b/apps/api/test/scripts/json-results.reporter.js @@ -0,0 +1,40 @@ +/** + * Minimal Jest reporter that writes the run's `AggregatedResult` to a + * JSON file at the end of the run. The default reporter is left in + * place by passing `--reporters=default --reporters=this-file` on the + * CLI, so the console output you normally see is unchanged — this + * reporter just adds a structured artifact that the dashboard + * generator can consume. + * + * Output path comes from the `JEST_RESULTS_OUTPUT` env var (resolved + * relative to the current working directory). If unset, defaults to + * `jest-results.json` in the CWD. + * + * Reason for env var instead of reporter options: Jest's CLI + * `--reporters` flag does not support inline option payloads, only + * the `reporters: [name, options]` tuple syntax inside a config file. + * Using an env var keeps the reporter usable from both CLI and config. + */ +const fs = require('fs'); +const path = require('path'); + +class JsonResultsReporter { + constructor(_globalConfig, options) { + const target = + process.env.JEST_RESULTS_OUTPUT ?? + options?.outputFile ?? + 'jest-results.json'; + this._outputFile = path.resolve(target); + } + + onRunComplete(_contexts, results) { + fs.mkdirSync(path.dirname(this._outputFile), { recursive: true }); + fs.writeFileSync( + this._outputFile, + JSON.stringify(results, null, 2), + 'utf8', + ); + } +} + +module.exports = JsonResultsReporter; diff --git a/apps/api/test/teams/teams.controller.spec.ts b/apps/api/test/teams/teams.controller.spec.ts deleted file mode 100644 index fe6668a3..00000000 --- a/apps/api/test/teams/teams.controller.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { TeamsService } from '../../src/modules/identity/application/teams.service'; -import { TeamsRepository } from '../../src/modules/identity/infrastructure/prisma-repositories/teams.repository'; -import { TeamsController } from '../../src/modules/identity/presentation/http/controllers/teams.controller'; - -describe('TeamsController', () => { - let controller: TeamsController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [TeamsController], - providers: [ - TeamsService, - { - provide: TeamsRepository, - useValue: { - create: jest.fn(), - findAll: jest.fn(), - findById: jest.fn(), - updateById: jest.fn(), - deleteById: jest.fn(), - }, - }, - ], - }).compile(); - - controller = module.get(TeamsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/api/test/teams/teams.service.spec.ts b/apps/api/test/teams/teams.service.spec.ts deleted file mode 100644 index b260de3a..00000000 --- a/apps/api/test/teams/teams.service.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { TeamsService } from '../../src/modules/identity/application/teams.service'; -import { TeamsRepository } from '../../src/modules/identity/infrastructure/prisma-repositories/teams.repository'; - -describe('TeamsService', () => { - let service: TeamsService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - TeamsService, - { - provide: TeamsRepository, - useValue: { - create: jest.fn(), - findAll: jest.fn(), - findById: jest.fn(), - updateById: jest.fn(), - deleteById: jest.fn(), - }, - }, - ], - }).compile(); - - service = module.get(TeamsService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/api/test/unit/contexts/feedback360/application/constants/cycle-stage-transitions.spec.ts b/apps/api/test/unit/contexts/feedback360/application/constants/cycle-stage-transitions.spec.ts new file mode 100644 index 00000000..0ddb5382 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/constants/cycle-stage-transitions.spec.ts @@ -0,0 +1,82 @@ +import { CycleStage } from '@intra/shared-kernel'; +import { + CYCLE_STAGE_TRANSITIONS, + TRANSITION_REASONS, +} from 'src/contexts/feedback360/application/constants/cycle-stage-transitions'; + +describe('CYCLE_STAGE_TRANSITIONS', () => { + it('allows NEW to move to ACTIVE or CANCELED only', () => { + expect(CYCLE_STAGE_TRANSITIONS[CycleStage.NEW]).toEqual([ + CycleStage.ACTIVE, + CycleStage.CANCELED, + ]); + }); + + it('allows ACTIVE to move to FINISHED or CANCELED only', () => { + expect(CYCLE_STAGE_TRANSITIONS[CycleStage.ACTIVE]).toEqual([ + CycleStage.FINISHED, + CycleStage.CANCELED, + ]); + }); + + it('allows FINISHED to move to PREPARING_REPORT or ARCHIVED only', () => { + expect(CYCLE_STAGE_TRANSITIONS[CycleStage.FINISHED]).toEqual([ + CycleStage.PREPARING_REPORT, + CycleStage.ARCHIVED, + ]); + }); + + it('allows PREPARING_REPORT to move to PUBLISHED or ARCHIVED only', () => { + expect(CYCLE_STAGE_TRANSITIONS[CycleStage.PREPARING_REPORT]).toEqual([ + CycleStage.PUBLISHED, + CycleStage.ARCHIVED, + ]); + }); + + it('allows PUBLISHED to move to ARCHIVED only', () => { + expect(CYCLE_STAGE_TRANSITIONS[CycleStage.PUBLISHED]).toEqual([ + CycleStage.ARCHIVED, + ]); + }); + + it('allows CANCELED to move to ARCHIVED only', () => { + expect(CYCLE_STAGE_TRANSITIONS[CycleStage.CANCELED]).toEqual([ + CycleStage.ARCHIVED, + ]); + }); + + it('treats ARCHIVED as a terminal stage', () => { + expect(CYCLE_STAGE_TRANSITIONS[CycleStage.ARCHIVED]).toEqual([]); + }); + + it('covers every CycleStage as an entry key', () => { + const stages = Object.values(CycleStage); + for (const stage of stages) { + expect(CYCLE_STAGE_TRANSITIONS[stage]).toBeDefined(); + } + }); + + it('never lists a stage as a transition from itself', () => { + for (const [from, targets] of Object.entries(CYCLE_STAGE_TRANSITIONS)) { + expect(targets).not.toContain(from); + } + }); +}); + +describe('TRANSITION_REASONS', () => { + it('exposes the canonical reason strings', () => { + expect(TRANSITION_REASONS.CYCLE_UPDATED).toBe('Cycle updated'); + expect(TRANSITION_REASONS.ALL_REVIEWS_COLLECTED).toBe( + 'All reviews collected', + ); + expect(TRANSITION_REASONS.DEADLINE_REACHED).toBe( + 'Cycle deadline has been reached', + ); + expect(TRANSITION_REASONS.FORCE_FINISH).toBe( + 'Manually finished by HR or Admin', + ); + expect(TRANSITION_REASONS.SYSTEM_AUTOMATED).toBe( + 'Automated system transition', + ); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/constants/response-status-transitions.spec.ts b/apps/api/test/unit/contexts/feedback360/application/constants/response-status-transitions.spec.ts new file mode 100644 index 00000000..bfc13746 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/constants/response-status-transitions.spec.ts @@ -0,0 +1,53 @@ +import { ResponseStatus } from '@intra/shared-kernel'; +import { + RESPONSE_STATUS_TRANSITIONS, + TRANSITION_REASONS, +} from 'src/contexts/feedback360/application/constants/response-status-transitions'; + +describe('RESPONSE_STATUS_TRANSITIONS', () => { + it('allows PENDING to move to IN_PROGRESS or CANCELED', () => { + expect(RESPONSE_STATUS_TRANSITIONS[ResponseStatus.PENDING]).toEqual([ + ResponseStatus.IN_PROGRESS, + ResponseStatus.CANCELED, + ]); + }); + + it('allows IN_PROGRESS to move to COMPLETED or CANCELED', () => { + expect(RESPONSE_STATUS_TRANSITIONS[ResponseStatus.IN_PROGRESS]).toEqual( + [ResponseStatus.COMPLETED, ResponseStatus.CANCELED], + ); + }); + + it('treats COMPLETED and CANCELED as terminal', () => { + expect(RESPONSE_STATUS_TRANSITIONS[ResponseStatus.COMPLETED]).toEqual( + [], + ); + expect(RESPONSE_STATUS_TRANSITIONS[ResponseStatus.CANCELED]).toEqual( + [], + ); + }); + + it('covers every ResponseStatus as an entry key', () => { + const statuses = Object.values(ResponseStatus); + for (const status of statuses) { + expect(RESPONSE_STATUS_TRANSITIONS[status]).toBeDefined(); + } + }); +}); + +describe('TRANSITION_REASONS (response)', () => { + it('exposes the canonical reason strings', () => { + expect(TRANSITION_REASONS.RESPONSE_ASSIGNED).toBe( + 'HR or Admin assigned review to respondent', + ); + expect(TRANSITION_REASONS.RESPONSE_ADDED).toBe( + 'Respondent added response', + ); + expect(TRANSITION_REASONS.RESPONSE_CANCELED).toBe( + 'HR or Admin or Respondent canceled review assignment', + ); + expect(TRANSITION_REASONS.SYSTEM_AUTOMATED).toBe( + 'Automated system transition', + ); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/constants/review-stage-transitions.spec.ts b/apps/api/test/unit/contexts/feedback360/application/constants/review-stage-transitions.spec.ts new file mode 100644 index 00000000..e90a03db --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/constants/review-stage-transitions.spec.ts @@ -0,0 +1,108 @@ +import { ReviewStage } from '@intra/shared-kernel'; +import { + REVIEW_STAGE_TRANSITIONS, + TRANSITION_REASONS, +} from 'src/contexts/feedback360/application/constants/review-stage-transitions'; + +describe('REVIEW_STAGE_TRANSITIONS', () => { + it('allows NEW to move to SELF_ASSESSMENT or CANCELED only', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.NEW]).toEqual([ + ReviewStage.SELF_ASSESSMENT, + ReviewStage.CANCELED, + ]); + }); + + it('allows SELF_ASSESSMENT to move to WAITING_TO_START, IN_PROGRESS, or CANCELED', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.SELF_ASSESSMENT]).toEqual([ + ReviewStage.WAITING_TO_START, + ReviewStage.IN_PROGRESS, + ReviewStage.CANCELED, + ]); + }); + + it('allows WAITING_TO_START to move to IN_PROGRESS, FINISHED, or CANCELED', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.WAITING_TO_START]).toEqual([ + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ReviewStage.CANCELED, + ]); + }); + + it('allows IN_PROGRESS to move to FINISHED or CANCELED', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.IN_PROGRESS]).toEqual([ + ReviewStage.FINISHED, + ReviewStage.CANCELED, + ]); + }); + + it('allows FINISHED to move to PREPARING_REPORT only', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.FINISHED]).toEqual([ + ReviewStage.PREPARING_REPORT, + ]); + }); + + it('allows PREPARING_REPORT to move into the report pipeline', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.PREPARING_REPORT]).toEqual([ + ReviewStage.PROCESSING_BY_HR, + ReviewStage.PUBLISHED, + ReviewStage.ANALYSIS, + ReviewStage.ARCHIVED, + ]); + }); + + it('allows PROCESSING_BY_HR to move to PUBLISHED, ANALYSIS, or ARCHIVED', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.PROCESSING_BY_HR]).toEqual([ + ReviewStage.PUBLISHED, + ReviewStage.ANALYSIS, + ReviewStage.ARCHIVED, + ]); + }); + + it('allows PUBLISHED to move to ANALYSIS or ARCHIVED', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.PUBLISHED]).toEqual([ + ReviewStage.ANALYSIS, + ReviewStage.ARCHIVED, + ]); + }); + + it('allows ANALYSIS to move to ARCHIVED only', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.ANALYSIS]).toEqual([ + ReviewStage.ARCHIVED, + ]); + }); + + it('allows CANCELED to move to ARCHIVED only', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.CANCELED]).toEqual([ + ReviewStage.ARCHIVED, + ]); + }); + + it('treats ARCHIVED as a terminal stage', () => { + expect(REVIEW_STAGE_TRANSITIONS[ReviewStage.ARCHIVED]).toEqual([]); + }); + + it('covers every ReviewStage as an entry key', () => { + const stages = Object.values(ReviewStage); + for (const stage of stages) { + expect(REVIEW_STAGE_TRANSITIONS[stage]).toBeDefined(); + } + }); +}); + +describe('TRANSITION_REASONS (review)', () => { + it('exposes the canonical reason strings', () => { + expect(TRANSITION_REASONS.REVIEW_UPDATED).toBe('Review updated'); + expect(TRANSITION_REASONS.ALL_RESPONSES_COLLECTED).toBe( + 'All responses collected from respondents', + ); + expect(TRANSITION_REASONS.DEADLINE_REACHED).toBe( + 'Review deadline has been reached', + ); + expect(TRANSITION_REASONS.FORCE_FINISH).toBe( + 'Manually finished by HR or Admin', + ); + expect(TRANSITION_REASONS.SYSTEM_AUTOMATED).toBe( + 'Automated system transition', + ); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/events/cycle-stage-processed.event.spec.ts b/apps/api/test/unit/contexts/feedback360/application/events/cycle-stage-processed.event.spec.ts new file mode 100644 index 00000000..47f86448 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/events/cycle-stage-processed.event.spec.ts @@ -0,0 +1,45 @@ +import { CycleStage } from '@intra/shared-kernel'; +import { CycleStageChangedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-changed.event'; +import { CycleStageProcessedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-processed.event'; + +describe('CycleStageProcessedEvent', () => { + describe('fromCycleStageChangedEvent', () => { + it('maps the toStage to the processed-event currentStage', () => { + const source = new CycleStageChangedEvent( + 42, + CycleStage.NEW, + CycleStage.ACTIVE, + 7, + 'HR Manager', + 'Cycle updated', + ); + + const event = + CycleStageProcessedEvent.fromCycleStageChangedEvent(source); + + expect(event).toBeInstanceOf(CycleStageProcessedEvent); + expect(event.cycleId).toBe(42); + expect(event.currentStage).toBe(CycleStage.ACTIVE); + expect(event.changedById).toBe(7); + expect(event.changedByName).toBe('HR Manager'); + expect(event.reason).toBe('Cycle updated'); + }); + + it('forwards optional actor/reason fields when undefined', () => { + const source = new CycleStageChangedEvent( + 10, + CycleStage.NEW, + CycleStage.ACTIVE, + ); + + const event = + CycleStageProcessedEvent.fromCycleStageChangedEvent(source); + + expect(event.cycleId).toBe(10); + expect(event.currentStage).toBe(CycleStage.ACTIVE); + expect(event.changedById).toBeUndefined(); + expect(event.changedByName).toBeUndefined(); + expect(event.reason).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/events/review-stage-processed.event.spec.ts b/apps/api/test/unit/contexts/feedback360/application/events/review-stage-processed.event.spec.ts new file mode 100644 index 00000000..b4adf09d --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/events/review-stage-processed.event.spec.ts @@ -0,0 +1,45 @@ +import { ReviewStage } from '@intra/shared-kernel'; +import { ReviewStageChangedEvent } from 'src/contexts/feedback360/application/events/review-stage-changed.event'; +import { ReviewStageProcessedEvent } from 'src/contexts/feedback360/application/events/review-stage-processed.event'; + +describe('ReviewStageProcessedEvent', () => { + describe('fromReviewStageChangedEvent', () => { + it('maps the toStage to the processed-event currentStage', () => { + const source = new ReviewStageChangedEvent( + 17, + ReviewStage.NEW, + ReviewStage.SELF_ASSESSMENT, + 8, + 'HR Manager', + 'Review updated', + ); + + const event = + ReviewStageProcessedEvent.fromReviewStageChangedEvent(source); + + expect(event).toBeInstanceOf(ReviewStageProcessedEvent); + expect(event.reviewId).toBe(17); + expect(event.currentStage).toBe(ReviewStage.SELF_ASSESSMENT); + expect(event.changedById).toBe(8); + expect(event.changedByName).toBe('HR Manager'); + expect(event.reason).toBe('Review updated'); + }); + + it('forwards optional actor/reason fields when undefined', () => { + const source = new ReviewStageChangedEvent( + 3, + ReviewStage.NEW, + ReviewStage.CANCELED, + ); + + const event = + ReviewStageProcessedEvent.fromReviewStageChangedEvent(source); + + expect(event.reviewId).toBe(3); + expect(event.currentStage).toBe(ReviewStage.CANCELED); + expect(event.changedById).toBeUndefined(); + expect(event.changedByName).toBeUndefined(); + expect(event.reason).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/listeners/cycle-stage.listener.spec.ts b/apps/api/test/unit/contexts/feedback360/application/listeners/cycle-stage.listener.spec.ts new file mode 100644 index 00000000..b496d6b9 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/listeners/cycle-stage.listener.spec.ts @@ -0,0 +1,119 @@ +import { CycleStage } from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Test } from '@nestjs/testing'; +import { CycleStageChangedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-changed.event'; +import { CycleStageListener } from 'src/contexts/feedback360/application/listeners/cycle-stage.listener'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { ClusterScoreAnalyticsService } from 'src/contexts/feedback360/application/services/cluster-score-analytics.service'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('CycleStageListener', () => { + let listener: CycleStageListener; + let analytics: any; + let reviews: any; + let eventEmitter: any; + + beforeEach(async () => { + analytics = { generateAnalyticsForCycle: jest.fn() }; + reviews = { listByCycleId: jest.fn() }; + eventEmitter = { emit: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + CycleStageListener, + { + provide: ClusterScoreAnalyticsService, + useValue: analytics, + }, + { provide: REVIEW_REPOSITORY, useValue: reviews }, + { provide: EventEmitter2, useValue: eventEmitter }, + ], + }).compile(); + + listener = module.get(CycleStageListener); + }); + + describe('handleCycleStageChanged', () => { + it('emits cycle.stage.processed without analytics when the new stage is not FINISHED', async () => { + const event = new CycleStageChangedEvent( + 1, + CycleStage.NEW, + CycleStage.ACTIVE, + ); + + await listener.handleCycleStageChanged(event); + + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'cycle.stage.processed', + expect.objectContaining({ + cycleId: 1, + currentStage: CycleStage.ACTIVE, + }), + ); + expect(analytics.generateAnalyticsForCycle).not.toHaveBeenCalled(); + }); + + it('does not run analytics when the cycle has no reviews', async () => { + reviews.listByCycleId.mockResolvedValue([]); + const event = new CycleStageChangedEvent( + 1, + CycleStage.ACTIVE, + CycleStage.FINISHED, + ); + + await listener.handleCycleStageChanged(event); + + expect(analytics.generateAnalyticsForCycle).not.toHaveBeenCalled(); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'cycle.stage.processed', + expect.objectContaining({ currentStage: CycleStage.FINISHED }), + ); + }); + + it('runs analytics generation and emits the processed event when FINISHED with reviews', async () => { + reviews.listByCycleId.mockResolvedValue([{ id: 1 }]); + analytics.generateAnalyticsForCycle.mockResolvedValue(undefined); + const event = new CycleStageChangedEvent( + 1, + CycleStage.ACTIVE, + CycleStage.FINISHED, + ); + + await listener.handleCycleStageChanged(event); + + expect(analytics.generateAnalyticsForCycle).toHaveBeenCalledWith(1); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'cycle.stage.processed', + expect.objectContaining({ currentStage: CycleStage.FINISHED }), + ); + }); + + it('still emits the processed event when analytics generation fails', async () => { + reviews.listByCycleId.mockResolvedValue([{ id: 1 }]); + analytics.generateAnalyticsForCycle.mockRejectedValue( + new Error('boom'), + ); + const event = new CycleStageChangedEvent( + 1, + CycleStage.ACTIVE, + CycleStage.FINISHED, + ); + + await listener.handleCycleStageChanged(event); + + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'cycle.stage.processed', + expect.objectContaining({ currentStage: CycleStage.FINISHED }), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/listeners/respondent-status.listener.spec.ts b/apps/api/test/unit/contexts/feedback360/application/listeners/respondent-status.listener.spec.ts new file mode 100644 index 00000000..069475de --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/listeners/respondent-status.listener.spec.ts @@ -0,0 +1,296 @@ +import { + RespondentCategory, + ResponseStatus, + ReviewStage, +} from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Test } from '@nestjs/testing'; +import { TRANSITION_REASONS } from 'src/contexts/feedback360/application/constants/review-stage-transitions'; +import { RespondentStatusChangedEvent } from 'src/contexts/feedback360/application/events/respondent-status-changed.event'; +import { + RespondentStatusListener, + SelfAssessmentCompletedListener, +} from 'src/contexts/feedback360/application/listeners/respondent-status.listener'; +import { RESPONDENT_REPOSITORY } from 'src/contexts/feedback360/application/ports/respondent.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +function buildRespondent( + overrides: Partial[0]> = {}, +): RespondentDomain { + return RespondentDomain.create({ + id: 1, + reviewId: 1, + respondentId: 200, + fullName: 'R', + category: RespondentCategory.TEAM, + positionId: 1, + positionTitle: 'Eng', + ...overrides, + }); +} + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 1, + rateeId: 1, + rateeFullName: 'Ratee', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + ...overrides, + }); +} + +describe('RespondentStatusListener', () => { + let listener: RespondentStatusListener; + let respondents: any; + let reviewService: any; + let eventEmitter: any; + + beforeEach(async () => { + respondents = { listByReview: jest.fn() }; + reviewService = { completeReview: jest.fn() }; + eventEmitter = { emit: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + RespondentStatusListener, + { provide: RESPONDENT_REPOSITORY, useValue: respondents }, + { provide: ReviewService, useValue: reviewService }, + { provide: EventEmitter2, useValue: eventEmitter }, + ], + }).compile(); + + listener = module.get(RespondentStatusListener); + }); + + it('ignores transitions that are not COMPLETED or CANCELED', async () => { + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.PENDING, + ResponseStatus.IN_PROGRESS, + ); + + await listener.handleRespondentStatusChanged(event); + + expect(respondents.listByReview).not.toHaveBeenCalled(); + expect(reviewService.completeReview).not.toHaveBeenCalled(); + }); + + it('completes the review when all respondents are COMPLETED or CANCELED', async () => { + respondents.listByReview.mockResolvedValue([ + buildRespondent({ responseStatus: ResponseStatus.COMPLETED }), + buildRespondent({ responseStatus: ResponseStatus.CANCELED }), + ]); + + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ); + + await listener.handleRespondentStatusChanged(event); + + expect(reviewService.completeReview).toHaveBeenCalledWith(1); + }); + + it('does not complete the review if any response is still pending', async () => { + respondents.listByReview.mockResolvedValue([ + buildRespondent({ responseStatus: ResponseStatus.COMPLETED }), + buildRespondent({ responseStatus: ResponseStatus.PENDING }), + ]); + + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ); + + await listener.handleRespondentStatusChanged(event); + + expect(reviewService.completeReview).not.toHaveBeenCalled(); + }); + + it('swallows errors when checking completion fails', async () => { + respondents.listByReview.mockRejectedValue(new Error('boom')); + + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ); + + await expect( + listener.handleRespondentStatusChanged(event), + ).resolves.toBeUndefined(); + }); +}); + +describe('SelfAssessmentCompletedListener', () => { + let listener: SelfAssessmentCompletedListener; + let respondents: any; + let reviews: any; + let reviewService: any; + + beforeEach(async () => { + respondents = { getById: jest.fn() }; + reviews = { findById: jest.fn() }; + reviewService = { changeReviewStage: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + SelfAssessmentCompletedListener, + { provide: RESPONDENT_REPOSITORY, useValue: respondents }, + { provide: REVIEW_REPOSITORY, useValue: reviews }, + { provide: ReviewService, useValue: reviewService }, + ], + }).compile(); + + listener = module.get(SelfAssessmentCompletedListener); + }); + + it('ignores transitions that are not COMPLETED', async () => { + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.PENDING, + ResponseStatus.IN_PROGRESS, + ); + + await listener.handleRespondentCompleted(event); + + expect(respondents.getById).not.toHaveBeenCalled(); + }); + + it('ignores respondents that are not SELF_ASSESSMENT category', async () => { + respondents.getById.mockResolvedValue( + buildRespondent({ category: RespondentCategory.TEAM }), + ); + + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ); + + await listener.handleRespondentCompleted(event); + + expect(reviewService.changeReviewStage).not.toHaveBeenCalled(); + }); + + it('logs a warning and returns when the review cannot be found', async () => { + respondents.getById.mockResolvedValue( + buildRespondent({ + category: RespondentCategory.SELF_ASSESSMENT, + }), + ); + reviews.findById.mockResolvedValue(null); + + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ); + + await listener.handleRespondentCompleted(event); + + expect(reviewService.changeReviewStage).not.toHaveBeenCalled(); + }); + + it('skips when the review is not in SELF_ASSESSMENT stage', async () => { + respondents.getById.mockResolvedValue( + buildRespondent({ + category: RespondentCategory.SELF_ASSESSMENT, + }), + ); + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ); + + await listener.handleRespondentCompleted(event); + + expect(reviewService.changeReviewStage).not.toHaveBeenCalled(); + }); + + it('advances the review from SELF_ASSESSMENT to IN_PROGRESS on self-assessment completion', async () => { + respondents.getById.mockResolvedValue( + buildRespondent({ + category: RespondentCategory.SELF_ASSESSMENT, + }), + ); + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.SELF_ASSESSMENT }), + ); + + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + 7, + 'Ratee', + ); + + await listener.handleRespondentCompleted(event); + + expect(reviewService.changeReviewStage).toHaveBeenCalledWith( + 1, + ReviewStage.IN_PROGRESS, + 7, + 'Ratee', + TRANSITION_REASONS.SYSTEM_AUTOMATED, + ); + }); + + it('swallows errors from the review stage transition', async () => { + respondents.getById.mockResolvedValue( + buildRespondent({ + category: RespondentCategory.SELF_ASSESSMENT, + }), + ); + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.SELF_ASSESSMENT }), + ); + reviewService.changeReviewStage.mockRejectedValue(new Error('boom')); + + const event = new RespondentStatusChangedEvent( + 1, + 10, + ResponseStatus.IN_PROGRESS, + ResponseStatus.COMPLETED, + ); + + await expect( + listener.handleRespondentCompleted(event), + ).resolves.toBeUndefined(); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/listeners/review-stage.listener.spec.ts b/apps/api/test/unit/contexts/feedback360/application/listeners/review-stage.listener.spec.ts new file mode 100644 index 00000000..70affe11 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/listeners/review-stage.listener.spec.ts @@ -0,0 +1,174 @@ +import { ReviewStage } from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Test } from '@nestjs/testing'; +import { ReviewStageChangedEvent } from 'src/contexts/feedback360/application/events/review-stage-changed.event'; +import { ReviewStageListener } from 'src/contexts/feedback360/application/listeners/review-stage.listener'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 1, + rateeId: 1, + rateeFullName: 'R', + rateePositionId: 1, + rateePositionTitle: 'Eng', + hrId: 2, + hrFullName: 'HR', + cycleId: 50, + ...overrides, + }); +} + +describe('ReviewStageListener', () => { + let listener: ReviewStageListener; + let cycles: any; + let reviews: any; + let eventEmitter: any; + + beforeEach(async () => { + cycles = { completeCycle: jest.fn() }; + reviews = { findById: jest.fn(), listByCycleId: jest.fn() }; + eventEmitter = { emit: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + ReviewStageListener, + { provide: CycleService, useValue: cycles }, + { provide: REVIEW_REPOSITORY, useValue: reviews }, + { provide: EventEmitter2, useValue: eventEmitter }, + ], + }).compile(); + + listener = module.get(ReviewStageListener); + }); + + it('still emits review.stage.processed when the new stage is not FINISHED', async () => { + const event = new ReviewStageChangedEvent( + 1, + ReviewStage.NEW, + ReviewStage.SELF_ASSESSMENT, + ); + + await listener.handleReviewStageChanged(event); + + expect(reviews.findById).not.toHaveBeenCalled(); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'review.stage.processed', + expect.objectContaining({ + currentStage: ReviewStage.SELF_ASSESSMENT, + }), + ); + }); + + it('warns and emits processed when the review is missing', async () => { + reviews.findById.mockResolvedValue(null); + + const event = new ReviewStageChangedEvent( + 1, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ); + + await listener.handleReviewStageChanged(event); + + expect(cycles.completeCycle).not.toHaveBeenCalled(); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'review.stage.processed', + expect.objectContaining({ currentStage: ReviewStage.FINISHED }), + ); + }); + + it('skips cycle completion when the review has no cycle attached', async () => { + reviews.findById.mockResolvedValue(buildReview({ cycleId: null })); + + const event = new ReviewStageChangedEvent( + 1, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ); + + await listener.handleReviewStageChanged(event); + + expect(cycles.completeCycle).not.toHaveBeenCalled(); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'review.stage.processed', + expect.any(Object), + ); + }); + + it('completes the cycle when all remaining reviews are past response collection', async () => { + reviews.findById.mockResolvedValue(buildReview()); + reviews.listByCycleId.mockResolvedValue([ + buildReview({ id: 1, stage: ReviewStage.FINISHED }), + buildReview({ id: 2, stage: ReviewStage.PUBLISHED }), + buildReview({ id: 3, stage: ReviewStage.CANCELED }), + ]); + + const event = new ReviewStageChangedEvent( + 1, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ); + + await listener.handleReviewStageChanged(event); + + expect(cycles.completeCycle).toHaveBeenCalledWith(50); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'review.stage.processed', + expect.any(Object), + ); + }); + + it('does not complete the cycle while another review is still active', async () => { + reviews.findById.mockResolvedValue(buildReview()); + reviews.listByCycleId.mockResolvedValue([ + buildReview({ id: 1, stage: ReviewStage.FINISHED }), + buildReview({ id: 2, stage: ReviewStage.IN_PROGRESS }), + ]); + + const event = new ReviewStageChangedEvent( + 1, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ); + + await listener.handleReviewStageChanged(event); + + expect(cycles.completeCycle).not.toHaveBeenCalled(); + }); + + it('still emits review.stage.processed when completeCycle fails', async () => { + reviews.findById.mockResolvedValue(buildReview()); + reviews.listByCycleId.mockResolvedValue([ + buildReview({ id: 1, stage: ReviewStage.FINISHED }), + ]); + cycles.completeCycle.mockRejectedValue(new Error('boom')); + + const event = new ReviewStageChangedEvent( + 1, + ReviewStage.IN_PROGRESS, + ReviewStage.FINISHED, + ); + + await listener.handleReviewStageChanged(event); + + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'review.stage.processed', + expect.any(Object), + ); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/services/cluster-score-analytics.service.spec.ts b/apps/api/test/unit/contexts/feedback360/application/services/cluster-score-analytics.service.spec.ts new file mode 100644 index 00000000..ab47d863 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/services/cluster-score-analytics.service.spec.ts @@ -0,0 +1,325 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import Decimal from 'decimal.js'; +import { CLUSTER_SCORE_ANALYTICS_REPOSITORY } from 'src/contexts/feedback360/application/ports/cluster-score-analytics.repository.port'; +import { CLUSTER_SCORE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cluster-score.repository.port'; +import { ClusterScoreAnalyticsService } from 'src/contexts/feedback360/application/services/cluster-score-analytics.service'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ClusterScoreAnalyticsDomain } from 'src/contexts/feedback360/domain/cluster-score-analytics.domain'; +import { ClusterScoreDomain } from 'src/contexts/feedback360/domain/cluster-score.domain'; +import { ClusterService } from 'src/contexts/library/application/services/cluster.service'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; + +function buildCluster(id: number, competenceId: number): ClusterDomain { + return ClusterDomain.create({ + id, + competenceId, + lowerBound: 0, + upperBound: 5, + title: `Cluster ${id}`, + description: 'desc', + }); +} + +function buildClusterScore( + clusterId: number, + rateeId: number, + score: number | string, +): ClusterScoreDomain { + return ClusterScoreDomain.create({ + cycleId: 100, + clusterId, + rateeId, + reviewId: rateeId, + score, + answersCount: 1, + }); +} + +describe('ClusterScoreAnalyticsService', () => { + let service: ClusterScoreAnalyticsService; + let analytics: any; + let clusterScores: any; + let cycles: any; + let clusters: any; + + beforeEach(async () => { + analytics = { + upsert: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + getByCycleId: jest.fn(), + }; + clusterScores = { list: jest.fn() }; + cycles = { getById: jest.fn() }; + clusters = { search: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + ClusterScoreAnalyticsService, + { + provide: CLUSTER_SCORE_ANALYTICS_REPOSITORY, + useValue: analytics, + }, + { + provide: CLUSTER_SCORE_REPOSITORY, + useValue: clusterScores, + }, + { provide: CycleService, useValue: cycles }, + { provide: ClusterService, useValue: clusters }, + ], + }).compile(); + + service = module.get(ClusterScoreAnalyticsService); + }); + + describe('upsert', () => { + const baseInput = { + cycleId: 100, + clusterId: 2, + employeesCount: 12, + employeeDensity: 0.5, + lowerBound: 0, + upperBound: 5, + minScore: 1, + maxScore: 4, + averageScore: 2.5, + }; + + it('verifies the cycle exists before validating and upserting', async () => { + analytics.upsert.mockImplementation( + async (d: ClusterScoreAnalyticsDomain) => d, + ); + + await service.upsert(baseInput); + + expect(cycles.getById).toHaveBeenCalledWith(100); + expect(analytics.upsert).toHaveBeenCalledTimes(1); + const domain = analytics.upsert.mock.calls[0][0]; + expect(domain).toBeInstanceOf(ClusterScoreAnalyticsDomain); + expect(domain.minScore.toNumber()).toBe(1); + expect(domain.maxScore.toNumber()).toBe(4); + }); + + it('throws when min score exceeds max score', async () => { + await expect( + service.upsert({ + ...baseInput, + minScore: 5, + maxScore: 1, + averageScore: 3, + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('throws when the average is outside [min, max]', async () => { + await expect( + service.upsert({ + ...baseInput, + minScore: 1, + maxScore: 4, + averageScore: 5, + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('throws when the lower bound is below the score minimum', async () => { + await expect( + service.upsert({ ...baseInput, lowerBound: -1 }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('throws when the upper bound exceeds the score maximum', async () => { + await expect( + service.upsert({ ...baseInput, upperBound: 6 }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('throws when the lower bound exceeds the upper bound', async () => { + await expect( + service.upsert({ + ...baseInput, + lowerBound: 4, + upperBound: 2, + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('search', () => { + it('delegates to the analytics repository', async () => { + analytics.search.mockResolvedValue([]); + await service.search({}); + expect(analytics.search).toHaveBeenCalledWith({}); + }); + }); + + describe('getById', () => { + it('throws NotFoundException when missing', async () => { + analytics.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('returns the row when present', async () => { + const row = { id: 1 } as any; + analytics.findById.mockResolvedValue(row); + await expect(service.getById(1)).resolves.toBe(row); + }); + }); + + describe('update', () => { + it('validates the merged min/max/avg against existing values', async () => { + const current = ClusterScoreAnalyticsDomain.create({ + id: 1, + cycleId: 100, + clusterId: 2, + lowerBound: 0, + upperBound: 5, + employeesCount: 12, + employeeDensity: 0.5, + minScore: 1, + maxScore: 4, + averageScore: 2, + }); + analytics.findById.mockResolvedValue(current); + + await expect( + service.update(1, { minScore: 5 }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('forwards only defined patch fields to updateById', async () => { + const current = ClusterScoreAnalyticsDomain.create({ + id: 1, + cycleId: 100, + clusterId: 2, + lowerBound: 0, + upperBound: 5, + employeesCount: 12, + employeeDensity: 0.5, + minScore: 1, + maxScore: 4, + averageScore: 2, + }); + analytics.findById.mockResolvedValue(current); + analytics.updateById.mockResolvedValue(current); + + await service.update(1, { employeesCount: 20 }); + expect(analytics.updateById).toHaveBeenCalledWith(1, { + employeesCount: 20, + }); + }); + }); + + describe('delete', () => { + it('throws NotFoundException when missing', async () => { + analytics.findById.mockResolvedValue(null); + await expect(service.delete(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + expect(analytics.deleteById).not.toHaveBeenCalled(); + }); + + it('deletes when present', async () => { + analytics.findById.mockResolvedValue({ id: 1 }); + await service.delete(1); + expect(analytics.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('getByCycleId', () => { + it('delegates to the analytics repository', async () => { + analytics.getByCycleId.mockResolvedValue([]); + await service.getByCycleId(100); + expect(analytics.getByCycleId).toHaveBeenCalledWith(100); + }); + }); + + describe('generateAnalyticsForCycle', () => { + it('computes min/max/average and employee density per cluster', async () => { + cycles.getById.mockResolvedValue({ id: 100 }); + clusters.search.mockResolvedValue([ + buildCluster(1, 10), + buildCluster(2, 10), + ]); + clusterScores.list.mockResolvedValue([ + buildClusterScore(1, 100, 4), + buildClusterScore(1, 101, 2), + buildClusterScore(2, 100, 3), + ]); + analytics.upsert.mockImplementation( + async (d: ClusterScoreAnalyticsDomain) => d, + ); + + await service.generateAnalyticsForCycle(100); + + // Two clusters with scores → two upserts + expect(analytics.upsert).toHaveBeenCalledTimes(2); + const cluster1 = analytics.upsert.mock.calls[0][0]; + expect(cluster1.minScore.toNumber()).toBe(2); + expect(cluster1.maxScore.toNumber()).toBe(4); + expect(cluster1.averageScore.toNumber()).toBe(3); + // 2 scores for cluster1 / 2 distinct ratees in competence 10 = 1 + expect(cluster1.employeeDensity.toNumber()).toBe(1); + expect(cluster1.employeesCount.toNumber()).toBe(2); + }); + + it('skips clusters with no scores', async () => { + cycles.getById.mockResolvedValue({ id: 100 }); + clusters.search.mockResolvedValue([buildCluster(1, 10)]); + clusterScores.list.mockResolvedValue([]); + + await service.generateAnalyticsForCycle(100); + + expect(analytics.upsert).not.toHaveBeenCalled(); + }); + + it('validates bounds and scores per cluster and surfaces the error', async () => { + cycles.getById.mockResolvedValue({ id: 100 }); + clusters.search.mockResolvedValue([ + ClusterDomain.create({ + id: 1, + competenceId: 10, + lowerBound: -1, // invalid + upperBound: 5, + title: 'C', + description: 'desc', + }), + ]); + clusterScores.list.mockResolvedValue([ + buildClusterScore(1, 100, 4), + ]); + + await expect( + service.generateAnalyticsForCycle(100), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('rounds scores to 4 decimal places when aggregating', async () => { + cycles.getById.mockResolvedValue({ id: 100 }); + clusters.search.mockResolvedValue([buildCluster(1, 10)]); + clusterScores.list.mockResolvedValue([ + buildClusterScore(1, 100, '1.111111'), + buildClusterScore(1, 101, '2.222222'), + buildClusterScore(1, 102, '3.333333'), + ]); + analytics.upsert.mockImplementation( + async (d: ClusterScoreAnalyticsDomain) => d, + ); + + await service.generateAnalyticsForCycle(100); + + const passed = analytics.upsert.mock.calls[0][0]; + // (1.111111 + 2.222222 + 3.333333) / 3 = 2.222222 → 4dp = 2.2222 + expect(passed.averageScore.equals(new Decimal('2.2222'))).toBe( + true, + ); + expect(passed.minScore.equals(new Decimal('1.1111'))).toBe(true); + expect(passed.maxScore.equals(new Decimal('3.3333'))).toBe(true); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/services/cycle.service.spec.ts b/apps/api/test/unit/contexts/feedback360/application/services/cycle.service.spec.ts new file mode 100644 index 00000000..55962598 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/services/cycle.service.spec.ts @@ -0,0 +1,411 @@ +import { CycleStage, ReviewStage, SYSTEM_ACTOR } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Test } from '@nestjs/testing'; +import { TRANSITION_REASONS } from 'src/contexts/feedback360/application/constants/cycle-stage-transitions'; +import { CycleStageChangedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-changed.event'; +import { CYCLE_STAGE_HISTORY_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle-stage-history.repository.port'; +import { CYCLE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { STRATEGIC_REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report.repository.port'; +import { PrismaService } from 'src/database/prisma.service'; + +type Mocked = { [K in keyof T]: jest.Mock }; + +function buildCycle( + overrides: Partial[0]> = {}, +): CycleDomain { + return CycleDomain.create({ + id: 1, + title: 'Q1 2025', + hrId: 7, + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-03-31T00:00:00.000Z'), + ...overrides, + }); +} + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 1, + rateeId: 1, + rateeFullName: 'Ratee', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + cycleId: 1, + ...overrides, + }); +} + +describe('CycleService', () => { + let service: CycleService; + let cycles: Mocked<{ + create: jest.Mock; + search: jest.Mock; + findById: jest.Mock; + updateById: jest.Mock; + deleteById: jest.Mock; + }>; + let stageHistory: Mocked<{ create: jest.Mock; findByCycleId: jest.Mock }>; + let reviews: Mocked<{ listByCycleId: jest.Mock }>; + let strategicReports: Mocked<{ findByCycleId: jest.Mock }>; + let prisma: { $transaction: jest.Mock }; + let eventEmitter: { emit: jest.Mock }; + let txUpdate: jest.Mock; + + beforeEach(async () => { + cycles = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + }; + stageHistory = { + create: jest.fn(), + findByCycleId: jest.fn(), + }; + reviews = { listByCycleId: jest.fn() }; + strategicReports = { findByCycleId: jest.fn() }; + eventEmitter = { emit: jest.fn() }; + txUpdate = jest.fn(); + prisma = { + $transaction: jest.fn(async (cb) => + cb({ cycle: { update: txUpdate } }), + ), + }; + + const module = await Test.createTestingModule({ + providers: [ + CycleService, + { provide: CYCLE_REPOSITORY, useValue: cycles }, + { + provide: CYCLE_STAGE_HISTORY_REPOSITORY, + useValue: stageHistory, + }, + { provide: REVIEW_REPOSITORY, useValue: reviews }, + { + provide: STRATEGIC_REPORT_REPOSITORY, + useValue: strategicReports, + }, + { provide: EventEmitter2, useValue: eventEmitter }, + { provide: PrismaService, useValue: prisma }, + ], + }).compile(); + + service = module.get(CycleService); + }); + + describe('create', () => { + it('creates a cycle and then returns it via getById', async () => { + const created = buildCycle(); + cycles.create.mockResolvedValue(created); + cycles.findById.mockResolvedValue(created); + + const result = await service.create({ + title: 'Q1 2025', + hrId: 7, + startDate: created.startDate, + endDate: created.endDate, + }); + + expect(cycles.create).toHaveBeenCalledTimes(1); + const passed = cycles.create.mock.calls[0][0] as CycleDomain; + expect(passed).toBeInstanceOf(CycleDomain); + expect(passed.stage).toBe(CycleStage.NEW); + expect(passed.isActive).toBe(true); + expect(cycles.findById).toHaveBeenCalledWith(created.id); + expect(result).toBe(created); + }); + + it('uses payload values for stage, isActive and minRespondentsThreshold when provided', async () => { + const created = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.create.mockResolvedValue(created); + cycles.findById.mockResolvedValue(created); + + await service.create({ + title: 'X', + hrId: 1, + stage: CycleStage.ACTIVE, + isActive: false, + minRespondentsThreshold: 9, + startDate: created.startDate, + endDate: created.endDate, + }); + + const passed = cycles.create.mock.calls[0][0] as CycleDomain; + expect(passed.stage).toBe(CycleStage.ACTIVE); + expect(passed.isActive).toBe(false); + expect(passed.minRespondentsThreshold).toBe(9); + }); + }); + + describe('search', () => { + it('delegates to the cycle repository', async () => { + const result = [buildCycle()]; + cycles.search.mockResolvedValue(result); + + const out = await service.search({ isActive: true }); + expect(cycles.search).toHaveBeenCalledWith({ isActive: true }); + expect(out).toBe(result); + }); + }); + + describe('getById', () => { + it('returns the cycle when found', async () => { + const cycle = buildCycle(); + cycles.findById.mockResolvedValue(cycle); + await expect(service.getById(1)).resolves.toBe(cycle); + }); + + it('throws NotFoundException when missing', async () => { + cycles.findById.mockResolvedValue(null); + await expect(service.getById(99)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('blocks title changes when a strategic report exists', async () => { + const cycle = buildCycle(); + cycles.findById.mockResolvedValue(cycle); + strategicReports.findByCycleId.mockResolvedValue({ id: 5 }); + + await expect( + service.update(1, { title: 'new title' }), + ).rejects.toBeInstanceOf(BadRequestException); + expect(cycles.updateById).not.toHaveBeenCalled(); + }); + + it('forwards only defined patch fields to updateById', async () => { + const cycle = buildCycle(); + cycles.findById.mockResolvedValue(cycle); + strategicReports.findByCycleId.mockResolvedValue(null); + cycles.updateById.mockResolvedValue({ + ...cycle, + description: 'desc', + }); + + await service.update(1, { description: 'desc' }); + + expect(cycles.updateById).toHaveBeenCalledWith(1, { + description: 'desc', + }); + }); + + it('blocks stage transition to FINISHED when reviews are still active', async () => { + const cycle = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.findById.mockResolvedValue(cycle); + strategicReports.findByCycleId.mockResolvedValue(null); + cycles.updateById.mockResolvedValue(cycle); + reviews.listByCycleId.mockResolvedValue([ + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ]); + + await expect( + service.update(1, { stage: CycleStage.FINISHED }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('runs completeCycle when patching to FINISHED with no active reviews', async () => { + const cycle = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.findById.mockResolvedValue(cycle); + strategicReports.findByCycleId.mockResolvedValue(null); + cycles.updateById.mockResolvedValue(cycle); + reviews.listByCycleId.mockResolvedValue([ + buildReview({ stage: ReviewStage.FINISHED }), + ]); + + await service.update(1, { stage: CycleStage.FINISHED }); + + // Stage transition happens via $transaction + expect(prisma.$transaction).toHaveBeenCalled(); + expect(txUpdate).toHaveBeenCalledWith({ + where: { id: 1 }, + data: { stage: CycleStage.FINISHED }, + }); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'cycle.stage.changed', + expect.any(CycleStageChangedEvent), + ); + }); + + it('skips changeStage when patch.stage equals current stage', async () => { + const cycle = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.findById.mockResolvedValue(cycle); + strategicReports.findByCycleId.mockResolvedValue(null); + cycles.updateById.mockResolvedValue(cycle); + + await service.update(1, { stage: CycleStage.ACTIVE }); + + expect(prisma.$transaction).not.toHaveBeenCalled(); + expect(eventEmitter.emit).not.toHaveBeenCalled(); + }); + + it('does not call changeStage when target stage is PUBLISHED', async () => { + const cycle = buildCycle({ stage: CycleStage.PREPARING_REPORT }); + cycles.findById.mockResolvedValue(cycle); + strategicReports.findByCycleId.mockResolvedValue(null); + cycles.updateById.mockResolvedValue(cycle); + reviews.listByCycleId.mockResolvedValue([]); + + await service.update(1, { stage: CycleStage.PUBLISHED }); + + expect(prisma.$transaction).not.toHaveBeenCalled(); + }); + }); + + describe('delete', () => { + it('throws NotFoundException when cycle is missing', async () => { + cycles.findById.mockResolvedValue(null); + await expect(service.delete(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + expect(cycles.deleteById).not.toHaveBeenCalled(); + }); + + it('deletes the cycle when it exists', async () => { + cycles.findById.mockResolvedValue(buildCycle()); + await service.delete(1); + expect(cycles.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('changeStage', () => { + it('throws on an invalid transition', async () => { + const cycle = buildCycle({ stage: CycleStage.ARCHIVED }); + cycles.findById.mockResolvedValue(cycle); + + await expect( + service.changeStage(1, CycleStage.ACTIVE), + ).rejects.toBeInstanceOf(BadRequestException); + expect(prisma.$transaction).not.toHaveBeenCalled(); + }); + + it('updates the stage, writes a history record, and emits the event', async () => { + const cycle = buildCycle({ stage: CycleStage.NEW }); + cycles.findById.mockResolvedValue(cycle); + + await service.changeStage( + 1, + CycleStage.ACTIVE, + 10, + 'Actor', + 'unit test reason', + ); + + expect(prisma.$transaction).toHaveBeenCalledTimes(1); + expect(txUpdate).toHaveBeenCalledWith({ + where: { id: 1 }, + data: { stage: CycleStage.ACTIVE }, + }); + expect(stageHistory.create).toHaveBeenCalledTimes(1); + const history = stageHistory.create.mock.calls[0][0]; + expect(history.cycleId).toBe(1); + expect(history.fromStage).toBe(CycleStage.NEW); + expect(history.toStage).toBe(CycleStage.ACTIVE); + expect(history.changedById).toBe(10); + expect(history.changedByName).toBe('Actor'); + expect(history.reason).toBe('unit test reason'); + + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'cycle.stage.changed', + expect.objectContaining({ + cycleId: 1, + fromStage: CycleStage.NEW, + toStage: CycleStage.ACTIVE, + changedById: 10, + changedByName: 'Actor', + reason: 'unit test reason', + }), + ); + }); + + it('uses SYSTEM_ACTOR defaults when actor information is omitted', async () => { + const cycle = buildCycle({ stage: CycleStage.NEW }); + cycles.findById.mockResolvedValue(cycle); + + await service.changeStage(1, CycleStage.ACTIVE); + + const history = stageHistory.create.mock.calls[0][0]; + expect(history.changedById).toBeNull(); // 0 || null + expect(history.changedByName).toBe(SYSTEM_ACTOR.NAME); + }); + }); + + describe('completeCycle', () => { + it('moves the cycle to FINISHED when all reviews are non-active', async () => { + const cycle = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.findById.mockResolvedValue(cycle); + reviews.listByCycleId.mockResolvedValue([ + buildReview({ stage: ReviewStage.FINISHED }), + buildReview({ stage: ReviewStage.PUBLISHED }), + ]); + + await service.completeCycle(1); + + expect(txUpdate).toHaveBeenCalledWith({ + where: { id: 1 }, + data: { stage: CycleStage.FINISHED }, + }); + const event = eventEmitter.emit.mock.calls[0][1]; + expect(event.toStage).toBe(CycleStage.FINISHED); + expect(event.reason).toBe(TRANSITION_REASONS.ALL_REVIEWS_COLLECTED); + }); + + it('throws when at least one review is still IN_PROGRESS', async () => { + const cycle = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.findById.mockResolvedValue(cycle); + reviews.listByCycleId.mockResolvedValue([ + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ]); + + await expect(service.completeCycle(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('does nothing when there are no reviews', async () => { + const cycle = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.findById.mockResolvedValue(cycle); + reviews.listByCycleId.mockResolvedValue([]); + + await service.completeCycle(1); + + expect(prisma.$transaction).not.toHaveBeenCalled(); + expect(eventEmitter.emit).not.toHaveBeenCalled(); + }); + }); + + describe('forceFinish', () => { + it('transitions the cycle to FINISHED with the FORCE_FINISH reason', async () => { + const cycle = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.findById.mockResolvedValue(cycle); + + await service.forceFinish(1, 8, 'HR Admin'); + + const history = stageHistory.create.mock.calls[0][0]; + expect(history.toStage).toBe(CycleStage.FINISHED); + expect(history.reason).toBe(TRANSITION_REASONS.FORCE_FINISH); + expect(history.changedById).toBe(8); + expect(history.changedByName).toBe('HR Admin'); + }); + }); + + describe('getStageHistory', () => { + it('delegates to the stageHistory repository', async () => { + stageHistory.findByCycleId.mockResolvedValue([{ id: 1 }]); + const result = await service.getStageHistory(42); + expect(stageHistory.findByCycleId).toHaveBeenCalledWith(42); + expect(result).toEqual([{ id: 1 }]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/services/review-scheduler.service.spec.ts b/apps/api/test/unit/contexts/feedback360/application/services/review-scheduler.service.spec.ts new file mode 100644 index 00000000..b4b6fc1d --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/services/review-scheduler.service.spec.ts @@ -0,0 +1,227 @@ +import { CycleStage, ReviewStage, SYSTEM_ACTOR } from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { TRANSITION_REASONS } from 'src/contexts/feedback360/application/constants/review-stage-transitions'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewSchedulerService } from 'src/contexts/feedback360/application/services/review-scheduler.service'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +function buildCycle( + overrides: Partial[0]> = {}, +): CycleDomain { + return CycleDomain.create({ + id: 1, + title: 'Q1', + hrId: 1, + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-12-31T00:00:00.000Z'), + ...overrides, + }); +} + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 100, + rateeId: 1, + rateeFullName: 'R', + rateePositionId: 1, + rateePositionTitle: 'Eng', + hrId: 2, + hrFullName: 'HR', + cycleId: 1, + ...overrides, + }); +} + +describe('ReviewSchedulerService', () => { + let service: ReviewSchedulerService; + let cycleService: any; + let reviewService: any; + + beforeEach(async () => { + cycleService = { + search: jest.fn(), + forceFinish: jest.fn(), + }; + reviewService = { + search: jest.fn(), + changeReviewStage: jest.fn(), + }; + + const module = await Test.createTestingModule({ + providers: [ + ReviewSchedulerService, + { provide: ReviewService, useValue: reviewService }, + { provide: CycleService, useValue: cycleService }, + ], + }).compile(); + + service = module.get(ReviewSchedulerService); + }); + + describe('checkReviewDeadlines', () => { + it('transitions IN_PROGRESS reviews to FINISHED when the cycle deadline has passed', async () => { + const cycle = buildCycle({ + responseDeadline: new Date('2024-01-01T00:00:00.000Z'), + }); + cycleService.search.mockResolvedValue([cycle]); + reviewService.search.mockResolvedValue([ + buildReview({ id: 11 }), + buildReview({ id: 22 }), + ]); + + await service.checkReviewDeadlines(); + + expect(cycleService.search).toHaveBeenCalledWith({ + isActive: true, + }); + expect(reviewService.search).toHaveBeenCalledWith( + { + cycleId: cycle.id, + stage: ReviewStage.IN_PROGRESS, + }, + expect.anything(), + ); + expect(reviewService.changeReviewStage).toHaveBeenCalledTimes(2); + expect(reviewService.changeReviewStage).toHaveBeenNthCalledWith( + 1, + 11, + ReviewStage.FINISHED, + SYSTEM_ACTOR.ID, + SYSTEM_ACTOR.NAME, + TRANSITION_REASONS.DEADLINE_REACHED, + ); + }); + + it('skips cycles without a responseDeadline', async () => { + cycleService.search.mockResolvedValue([ + buildCycle({ responseDeadline: null }), + ]); + await service.checkReviewDeadlines(); + expect(reviewService.search).not.toHaveBeenCalled(); + }); + + it('does nothing when the deadline is in the future', async () => { + cycleService.search.mockResolvedValue([ + buildCycle({ + responseDeadline: new Date( + Date.now() + 24 * 60 * 60 * 1000, + ), + }), + ]); + await service.checkReviewDeadlines(); + expect(reviewService.search).not.toHaveBeenCalled(); + }); + + it('continues processing remaining reviews when one transition fails', async () => { + cycleService.search.mockResolvedValue([ + buildCycle({ + responseDeadline: new Date('2024-01-01T00:00:00.000Z'), + }), + ]); + reviewService.search.mockResolvedValue([ + buildReview({ id: 11 }), + buildReview({ id: 22 }), + ]); + reviewService.changeReviewStage + .mockRejectedValueOnce(new Error('boom')) + .mockResolvedValueOnce(undefined); + + await expect( + service.checkReviewDeadlines(), + ).resolves.toBeUndefined(); + expect(reviewService.changeReviewStage).toHaveBeenCalledTimes(2); + }); + + it('swallows errors from the cycle search and does not throw', async () => { + cycleService.search.mockRejectedValue(new Error('boom')); + await expect( + service.checkReviewDeadlines(), + ).resolves.toBeUndefined(); + }); + }); + + describe('checkCycleDeadlines', () => { + it('force-finishes cycles whose endDate has passed', async () => { + const cycle = buildCycle({ + stage: CycleStage.ACTIVE, + endDate: new Date('2024-01-01T00:00:00.000Z'), + }); + cycleService.search.mockResolvedValue([cycle]); + + await service.checkCycleDeadlines(); + + expect(cycleService.search).toHaveBeenCalledWith({ + stage: CycleStage.ACTIVE, + isActive: true, + }); + expect(cycleService.forceFinish).toHaveBeenCalledWith(cycle.id); + }); + + it('force-finishes cycles whose responseDeadline has passed even when endDate is in the future', async () => { + const cycle = buildCycle({ + stage: CycleStage.ACTIVE, + endDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), + responseDeadline: new Date('2024-01-01T00:00:00.000Z'), + }); + cycleService.search.mockResolvedValue([cycle]); + + await service.checkCycleDeadlines(); + + expect(cycleService.forceFinish).toHaveBeenCalledWith(cycle.id); + }); + + it('skips cycles where no deadline has passed yet', async () => { + const cycle = buildCycle({ + stage: CycleStage.ACTIVE, + endDate: new Date(Date.now() + 24 * 60 * 60 * 1000), + responseDeadline: new Date(Date.now() + 24 * 60 * 60 * 1000), + }); + cycleService.search.mockResolvedValue([cycle]); + + await service.checkCycleDeadlines(); + + expect(cycleService.forceFinish).not.toHaveBeenCalled(); + }); + + it('continues processing remaining cycles when one fails to finish', async () => { + const a = buildCycle({ + id: 1, + endDate: new Date('2024-01-01T00:00:00.000Z'), + }); + const b = buildCycle({ + id: 2, + endDate: new Date('2024-01-01T00:00:00.000Z'), + }); + cycleService.search.mockResolvedValue([a, b]); + cycleService.forceFinish + .mockRejectedValueOnce(new Error('boom')) + .mockResolvedValueOnce(undefined); + + await expect( + service.checkCycleDeadlines(), + ).resolves.toBeUndefined(); + expect(cycleService.forceFinish).toHaveBeenCalledTimes(2); + }); + + it('swallows errors from the cycle search and does not throw', async () => { + cycleService.search.mockRejectedValue(new Error('boom')); + await expect( + service.checkCycleDeadlines(), + ).resolves.toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/application/services/review.service.spec.ts b/apps/api/test/unit/contexts/feedback360/application/services/review.service.spec.ts new file mode 100644 index 00000000..c75a77a4 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/application/services/review.service.spec.ts @@ -0,0 +1,1216 @@ +import { + AnswerType, + CycleStage, + IdentityRole, + RespondentCategory, + ResponseStatus, + ReviewStage, +} from '@intra/shared-kernel'; +import { + BadRequestException, + ForbiddenException, + NotFoundException, +} from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Test } from '@nestjs/testing'; +import { TRANSITION_REASONS as REVIEW_TRANSITION_REASONS } from 'src/contexts/feedback360/application/constants/review-stage-transitions'; +import { ReviewStageChangedEvent } from 'src/contexts/feedback360/application/events/review-stage-changed.event'; +import { ANSWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/answer.repository.port'; +import { CLUSTER_SCORE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cluster-score.repository.port'; +import { QUESTION_REPOSITORY } from 'src/contexts/feedback360/application/ports/question.repository.port'; +import { RESPONDENT_REPOSITORY } from 'src/contexts/feedback360/application/ports/respondent.repository.port'; +import { REVIEW_QUESTION_RELATION_REPOSITORY } from 'src/contexts/feedback360/application/ports/review-question-relation.repository.port'; +import { REVIEW_STAGE_HISTORY_REPOSITORY } from 'src/contexts/feedback360/application/ports/review-stage-history.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { REVIEWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/reviewer.repository.port'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { ClusterScoreDomain } from 'src/contexts/feedback360/domain/cluster-score.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { PrismaService } from 'src/database/prisma.service'; + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 1, + rateeId: 11, + rateeFullName: 'Ratee', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + cycleId: 50, + ...overrides, + }); +} + +function buildRespondent( + overrides: Partial[0]> = {}, +): RespondentDomain { + return RespondentDomain.create({ + id: 1, + reviewId: 1, + respondentId: 100, + fullName: 'John Doe', + category: RespondentCategory.TEAM, + positionId: 1, + positionTitle: 'Engineer', + ...overrides, + }); +} + +function buildUser( + overrides: Partial[0]> = {}, +): UserDomain { + return UserDomain.create({ + id: 200, + firstName: 'Actor', + lastName: 'User', + email: 'actor@example.com', + roles: [IdentityRole.HR], + ...overrides, + }); +} + +describe('ReviewService', () => { + let service: ReviewService; + let reviews: any; + let questions: any; + let questionRelations: any; + let answers: any; + let respondents: any; + let reviewers: any; + let clusterScores: any; + let stageHistory: any; + let questionTemplates: any; + let competences: any; + let cycles: any; + let prisma: any; + let eventEmitter: any; + let txReviewUpdate: jest.Mock; + + beforeEach(async () => { + reviews = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + listByCycleId: jest.fn(), + }; + questions = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + deleteById: jest.fn(), + }; + questionRelations = { + link: jest.fn(), + listByReview: jest.fn(), + unlink: jest.fn(), + }; + answers = { + create: jest.fn(), + list: jest.fn(), + getAnswersCountByRespondentCategories: jest.fn(), + }; + respondents = { + create: jest.fn(), + getById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + listByReview: jest.fn(), + }; + reviewers = { + create: jest.fn(), + listByReview: jest.fn(), + deleteById: jest.fn(), + }; + clusterScores = { + upsert: jest.fn(), + list: jest.fn(), + getById: jest.fn(), + getByCycleId: jest.fn(), + deleteById: jest.fn(), + }; + stageHistory = { + create: jest.fn(), + findByReviewId: jest.fn(), + }; + questionTemplates = { getById: jest.fn() }; + competences = { getById: jest.fn() }; + cycles = { getById: jest.fn() }; + eventEmitter = { emit: jest.fn() }; + txReviewUpdate = jest.fn(); + prisma = { + $transaction: jest.fn(async (cb) => + cb({ review: { update: txReviewUpdate } }), + ), + }; + + const module = await Test.createTestingModule({ + providers: [ + ReviewService, + { provide: REVIEW_REPOSITORY, useValue: reviews }, + { provide: QUESTION_REPOSITORY, useValue: questions }, + { + provide: REVIEW_QUESTION_RELATION_REPOSITORY, + useValue: questionRelations, + }, + { provide: ANSWER_REPOSITORY, useValue: answers }, + { provide: RESPONDENT_REPOSITORY, useValue: respondents }, + { provide: REVIEWER_REPOSITORY, useValue: reviewers }, + { provide: CLUSTER_SCORE_REPOSITORY, useValue: clusterScores }, + { + provide: REVIEW_STAGE_HISTORY_REPOSITORY, + useValue: stageHistory, + }, + { provide: PrismaService, useValue: prisma }, + { + provide: QuestionTemplateService, + useValue: questionTemplates, + }, + { provide: CompetenceService, useValue: competences }, + { provide: CycleService, useValue: cycles }, + { provide: EventEmitter2, useValue: eventEmitter }, + ], + }).compile(); + + service = module.get(ReviewService); + }); + + describe('create', () => { + it('verifies the cycle exists before creating the review when cycleId is provided', async () => { + const created = buildReview(); + reviews.create.mockResolvedValue(created); + reviews.findById.mockResolvedValue(created); + cycles.getById.mockResolvedValue({ id: 50, stage: CycleStage.NEW }); + + await service.create({ + rateeId: 11, + rateeFullName: 'Ratee', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + cycleId: 50, + }); + + expect(cycles.getById).toHaveBeenCalledWith(50); + expect(reviews.create).toHaveBeenCalledTimes(1); + expect(reviews.findById).toHaveBeenCalledWith(created.id); + }); + + it('does not call cycles.getById when no cycleId is provided', async () => { + const created = buildReview({ cycleId: null }); + reviews.create.mockResolvedValue(created); + reviews.findById.mockResolvedValue(created); + + await service.create({ + rateeId: 11, + rateeFullName: 'Ratee', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + }); + + expect(cycles.getById).not.toHaveBeenCalled(); + }); + }); + + describe('getById', () => { + it('throws NotFoundException when the review is missing', async () => { + reviews.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('returns the review when no actor is provided', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + await expect(service.getById(1)).resolves.toBe(review); + }); + + it('returns the review for admin/HR actors', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + const actor = buildUser({ roles: [IdentityRole.ADMIN] }); + await expect(service.getById(1, actor)).resolves.toBe(review); + }); + + it('returns the review when actor is the ratee', async () => { + const review = buildReview({ rateeId: 200 }); + reviews.findById.mockResolvedValue(review); + const actor = buildUser({ + id: 200, + roles: [IdentityRole.EMPLOYEE], + }); + await expect(service.getById(1, actor)).resolves.toBe(review); + }); + + it('returns the review when actor is the manager', async () => { + const review = buildReview({ managerId: 200 }); + reviews.findById.mockResolvedValue(review); + const actor = buildUser({ id: 200, roles: [IdentityRole.MANAGER] }); + await expect(service.getById(1, actor)).resolves.toBe(review); + }); + + it('returns the review when actor is a respondent', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + respondents.listByReview.mockResolvedValue([buildRespondent()]); + reviewers.listByReview.mockResolvedValue([]); + const actor = buildUser({ + id: 999, + roles: [IdentityRole.EMPLOYEE], + }); + await expect(service.getById(1, actor)).resolves.toBe(review); + }); + + it('returns the review when actor is a reviewer', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + respondents.listByReview.mockResolvedValue([]); + reviewers.listByReview.mockResolvedValue([{ id: 1 }]); + const actor = buildUser({ + id: 999, + roles: [IdentityRole.EMPLOYEE], + }); + await expect(service.getById(1, actor)).resolves.toBe(review); + }); + + it('throws ForbiddenException when actor has no access', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + respondents.listByReview.mockResolvedValue([]); + reviewers.listByReview.mockResolvedValue([]); + const actor = buildUser({ + id: 999, + roles: [IdentityRole.EMPLOYEE], + }); + await expect(service.getById(1, actor)).rejects.toBeInstanceOf( + ForbiddenException, + ); + }); + }); + + describe('search', () => { + it('filters out reviews the actor cannot read', async () => { + const accessible = buildReview({ id: 1, rateeId: 200 }); + const inaccessible = buildReview({ id: 2, rateeId: 5 }); + reviews.search.mockResolvedValue([accessible, inaccessible]); + respondents.listByReview.mockResolvedValue([]); + reviewers.listByReview.mockResolvedValue([]); + + const actor = buildUser({ + id: 200, + roles: [IdentityRole.EMPLOYEE], + }); + const result = await service.search({}, actor); + + expect(result).toEqual([accessible]); + }); + + it('returns all reviews for admin/HR actors', async () => { + const a = buildReview({ id: 1 }); + const b = buildReview({ id: 2 }); + reviews.search.mockResolvedValue([a, b]); + + const actor = buildUser({ roles: [IdentityRole.HR] }); + const result = await service.search({}, actor); + + expect(result).toEqual([a, b]); + }); + }); + + describe('update', () => { + it('rejects updates when the review is not NEW or PROCESSING_BY_HR', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + const actor = buildUser(); + await expect( + service.update(1, { hrNote: 'note' }, actor), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('rejects reassigning to a cycle outside NEW/ACTIVE', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.NEW }), + ); + cycles.getById.mockResolvedValue({ + id: 50, + stage: CycleStage.FINISHED, + }); + const actor = buildUser(); + await expect( + service.update(1, { cycleId: 50 }, actor), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('allows updating an editable field and forwards only defined fields', async () => { + const review = buildReview({ stage: ReviewStage.NEW }); + reviews.findById.mockResolvedValue(review); + reviews.updateById.mockResolvedValue({ + ...review, + hrNote: 'note', + }); + const actor = buildUser(); + + await service.update(1, { hrNote: 'note' }, actor); + + expect(reviews.updateById).toHaveBeenCalledWith(1, { + hrNote: 'note', + }); + }); + + it('runs changeReviewStage on a valid non-FINISHED stage patch', async () => { + const review = buildReview({ stage: ReviewStage.NEW }); + reviews.findById.mockResolvedValue(review); + reviews.updateById.mockResolvedValue(review); + + const actor = buildUser(); + await service.update( + 1, + { stage: ReviewStage.SELF_ASSESSMENT }, + actor, + ); + + // changeReviewStage uses prisma.$transaction + expect(prisma.$transaction).toHaveBeenCalled(); + expect(txReviewUpdate).toHaveBeenCalledWith({ + where: { id: 1 }, + data: { stage: ReviewStage.SELF_ASSESSMENT }, + }); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'review.stage.changed', + expect.any(ReviewStageChangedEvent), + ); + }); + }); + + describe('delete', () => { + it('throws when the review has answers already submitted', async () => { + reviews.findById.mockResolvedValue(buildReview()); + answers.list.mockResolvedValue([{ id: 1 }]); + await expect(service.delete(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('deletes when there are no answers', async () => { + reviews.findById.mockResolvedValue(buildReview()); + answers.list.mockResolvedValue([]); + await service.delete(1); + expect(reviews.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('createQuestion', () => { + it('rejects when the cycle is not in NEW/ACTIVE stage', async () => { + cycles.getById.mockResolvedValue({ + id: 50, + stage: CycleStage.FINISHED, + }); + await expect( + service.createQuestion({ + cycleId: 50, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('falls back to template fields when payload fields are missing', async () => { + cycles.getById.mockResolvedValue({ + id: 50, + stage: CycleStage.ACTIVE, + }); + questionTemplates.getById.mockResolvedValue( + QuestionTemplateDomain.create({ + id: 7, + title: 'Template title', + answerType: AnswerType.TEXT_FIELD, + competenceId: 3, + isForSelfassessment: true, + }), + ); + const created = QuestionDomain.create({ + id: 8, + title: 'Template title', + answerType: AnswerType.TEXT_FIELD, + competenceId: 3, + isForSelfassessment: true, + }); + questions.create.mockResolvedValue(created); + + const result = await service.createQuestion({ + cycleId: 50, + questionTemplateId: 7, + } as any); + + expect(result).toBe(created); + const passedDomain = questions.create.mock.calls[0][0]; + expect(passedDomain.title).toBe('Template title'); + expect(passedDomain.answerType).toBe(AnswerType.TEXT_FIELD); + expect(passedDomain.competenceId).toBe(3); + expect(passedDomain.isForSelfassessment).toBe(true); + }); + + it('throws when neither payload nor template provide a title or answer type', async () => { + await expect( + service.createQuestion({} as any), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('deleteQuestion', () => { + it('throws NotFoundException when the question does not exist', async () => { + questions.findById.mockResolvedValue(null); + await expect(service.deleteQuestion(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('rejects deletion when the cycle is not NEW/ACTIVE', async () => { + questions.findById.mockResolvedValue( + QuestionDomain.create({ + id: 1, + cycleId: 50, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + }), + ); + cycles.getById.mockResolvedValue({ + id: 50, + stage: CycleStage.FINISHED, + }); + await expect(service.deleteQuestion(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('deletes when the question has no cycle attached', async () => { + questions.findById.mockResolvedValue( + QuestionDomain.create({ + id: 1, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + }), + ); + await service.deleteQuestion(1); + expect(questions.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('attachQuestion', () => { + it('rejects when the review is not in NEW stage', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + await expect( + service.attachQuestion({ reviewId: 1, questionTemplateId: 7 }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('reuses an existing question snapshot when one already exists', async () => { + reviews.findById.mockResolvedValue(buildReview()); + const template = QuestionTemplateDomain.create({ + id: 7, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 3, + isForSelfassessment: false, + }); + questionTemplates.getById.mockResolvedValue(template); + competences.getById.mockResolvedValue( + CompetenceDomain.create({ + id: 3, + title: 'C', + code: 'C', + description: 'desc', + }), + ); + const existing = QuestionDomain.create({ + id: 9, + cycleId: 50, + questionTemplateId: 7, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 3, + }); + questions.search.mockResolvedValue([existing]); + questionRelations.link.mockImplementation( + async (relation: ReviewQuestionRelationDomain) => relation, + ); + + await service.attachQuestion({ + reviewId: 1, + questionTemplateId: 7, + }); + + expect(questions.create).not.toHaveBeenCalled(); + const linked = questionRelations.link.mock.calls[0][0]; + expect(linked).toBeInstanceOf(ReviewQuestionRelationDomain); + expect(linked.questionId).toBe(9); + }); + + it('creates a new question snapshot when none exists', async () => { + reviews.findById.mockResolvedValue(buildReview()); + const template = QuestionTemplateDomain.create({ + id: 7, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 3, + isForSelfassessment: false, + }); + questionTemplates.getById.mockResolvedValue(template); + competences.getById.mockResolvedValue( + CompetenceDomain.create({ + id: 3, + title: 'C', + code: 'C', + description: 'desc', + }), + ); + questions.search.mockResolvedValue([]); + questions.create.mockImplementation(async (q: QuestionDomain) => + QuestionDomain.create({ ...q, id: 42 }), + ); + questionRelations.link.mockImplementation( + async (relation: ReviewQuestionRelationDomain) => relation, + ); + + await service.attachQuestion({ + reviewId: 1, + questionTemplateId: 7, + }); + + expect(questions.create).toHaveBeenCalledTimes(1); + const linked = questionRelations.link.mock.calls[0][0]; + expect(linked.questionId).toBe(42); + }); + }); + + describe('detachQuestion', () => { + it('rejects detaching when the review has started', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + await expect(service.detachQuestion(1, 5)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('unlinks the relation when the review is NEW', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.NEW }), + ); + await service.detachQuestion(1, 5); + expect(questionRelations.unlink).toHaveBeenCalledWith(1, 5); + }); + }); + + describe('listQuestionRelations', () => { + it('returns the question relations after the access check passes', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + const relations = [{ id: 1 }]; + questionRelations.listByReview.mockResolvedValue(relations); + + const actor = buildUser({ roles: [IdentityRole.HR] }); + const result = await service.listQuestionRelations( + 1, + {} as any, + actor, + ); + + expect(questionRelations.listByReview).toHaveBeenCalledWith(1, {}); + expect(result).toBe(relations); + }); + }); + + describe('listQuestions', () => { + it('delegates to the questions repository', async () => { + questions.search.mockResolvedValue([{ id: 1 }]); + const result = await service.listQuestions({} as any); + expect(questions.search).toHaveBeenCalledWith({}); + expect(result).toEqual([{ id: 1 }]); + }); + }); + + describe('addAnswer', () => { + it('rejects adding an answer when the review is not in progress', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.NEW }), + ); + const actor = buildUser(); + await expect( + service.addAnswer( + { + reviewId: 1, + questionId: 1, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 5, + }, + actor, + ), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('persists the answer when the review is IN_PROGRESS', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + answers.create.mockImplementation(async (a: any) => ({ + ...a, + id: 99, + })); + + const actor = buildUser(); + await service.addAnswer( + { + reviewId: 1, + questionId: 2, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }, + actor, + ); + + expect(answers.create).toHaveBeenCalledTimes(1); + const created = answers.create.mock.calls[0][0]; + expect(created.reviewId).toBe(1); + expect(created.numericalValue).toBe(4); + }); + }); + + describe('listAnswers', () => { + it('returns answers filtered by respondent category', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + answers.list.mockResolvedValue([{ id: 1 }]); + + const actor = buildUser({ roles: [IdentityRole.HR] }); + const out = await service.listAnswers( + 1, + RespondentCategory.TEAM, + actor, + ); + + expect(answers.list).toHaveBeenCalledWith({ + reviewId: 1, + respondentCategory: RespondentCategory.TEAM, + }); + expect(out).toEqual([{ id: 1 }]); + }); + + it('allows the manager of the review to read answers', async () => { + const review = buildReview({ managerId: 5 }); + reviews.findById.mockResolvedValue(review); + answers.list.mockResolvedValue([]); + reviewers.listByReview.mockResolvedValue([]); + + const actor = buildUser({ + id: 5, + roles: [IdentityRole.MANAGER], + }); + + await expect( + service.listAnswers(1, undefined, actor), + ).resolves.toEqual([]); + }); + + it('allows a respondent who is also a reviewer of the review', async () => { + const review = buildReview({ rateeId: 999, managerId: 999 }); + reviews.findById.mockResolvedValue(review); + answers.list.mockResolvedValue([]); + reviewers.listByReview.mockResolvedValue([{ id: 1 }]); + + const actor = buildUser({ + id: 5, + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.listAnswers(1, undefined, actor), + ).resolves.toEqual([]); + }); + + it('throws ForbiddenException when the actor has no relation to the review', async () => { + const review = buildReview({ rateeId: 999, managerId: 999 }); + reviews.findById.mockResolvedValue(review); + reviewers.listByReview.mockResolvedValue([]); + respondents.listByReview.mockResolvedValue([]); + + const actor = buildUser({ + id: 5, + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.listAnswers(1, undefined, actor), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('countReviewAnswers', () => { + it('sums the answers count per respondent category', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + answers.getAnswersCountByRespondentCategories.mockResolvedValue([ + { answers: 3 }, + { answers: 2 }, + ]); + + const actor = buildUser({ roles: [IdentityRole.HR] }); + const count = await service.countReviewAnswers(1, actor); + + expect(count).toBe(5); + }); + + it('allows the ratee of the review to count answers', async () => { + const review = buildReview({ rateeId: 5 }); + reviews.findById.mockResolvedValue(review); + answers.getAnswersCountByRespondentCategories.mockResolvedValue([]); + + const actor = buildUser({ + id: 5, + roles: [IdentityRole.EMPLOYEE], + }); + + await expect(service.countReviewAnswers(1, actor)).resolves.toBe(0); + }); + }); + + describe('addRespondent', () => { + it('rejects adding to a review in FINISHED stage', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.FINISHED }), + ); + await expect( + service.addRespondent({ + reviewId: 1, + respondentId: 100, + fullName: 'John Doe', + category: RespondentCategory.TEAM, + responseStatus: ResponseStatus.PENDING, + positionId: 1, + positionTitle: 'Engineer', + } as any), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('persists the respondent for an in-progress review', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + respondents.create.mockImplementation( + async (r: RespondentDomain) => r, + ); + + await service.addRespondent({ + reviewId: 1, + respondentId: 100, + fullName: 'John Doe', + category: RespondentCategory.TEAM, + responseStatus: ResponseStatus.PENDING, + positionId: 1, + positionTitle: 'Engineer', + } as any); + + expect(respondents.create).toHaveBeenCalledTimes(1); + const passed = respondents.create.mock.calls[0][0]; + expect(passed).toBeInstanceOf(RespondentDomain); + expect(passed.fullName).toBe('John Doe'); + }); + }); + + describe('updateRespondent', () => { + it('forbids non-admin actors that are not the respondent themselves', async () => { + respondents.getById.mockResolvedValue( + buildRespondent({ respondentId: 999 }), + ); + const actor = buildUser({ + id: 200, + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.updateRespondent(1, { hrNote: 'x' } as any, actor), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + + it('rejects when the underlying review is not in an in-progress stage', async () => { + respondents.getById.mockResolvedValue( + buildRespondent({ respondentId: 200 }), + ); + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.NEW }), + ); + const actor = buildUser({ + id: 200, + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.updateRespondent(1, { hrNote: 'x' } as any, actor), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('updates fields and triggers a status transition when responseStatus is patched', async () => { + respondents.getById.mockResolvedValue( + buildRespondent({ + id: 1, + reviewId: 1, + respondentId: 200, + responseStatus: ResponseStatus.PENDING, + }), + ); + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + respondents.updateById.mockResolvedValue(buildRespondent()); + + const actor = buildUser({ + id: 200, + roles: [IdentityRole.EMPLOYEE], + }); + + await service.updateRespondent( + 1, + { responseStatus: ResponseStatus.IN_PROGRESS } as any, + actor, + ); + + // Two calls: 1) initial patch with responseStatus stripped, 2) inside changeRespondentStatus + expect(respondents.updateById).toHaveBeenCalledTimes(2); + expect(respondents.updateById).toHaveBeenLastCalledWith(1, { + responseStatus: ResponseStatus.IN_PROGRESS, + }); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'respondent.status.changed', + expect.objectContaining({ + reviewId: 1, + respondentRelationId: 1, + fromStatus: ResponseStatus.PENDING, + toStatus: ResponseStatus.IN_PROGRESS, + }), + ); + }); + }); + + describe('changeRespondentStatus', () => { + it('rejects when respondent does not belong to the review', async () => { + reviews.findById.mockResolvedValue(buildReview({ id: 1 })); + respondents.getById.mockResolvedValue( + buildRespondent({ id: 7, reviewId: 99 }), + ); + + await expect( + service.changeRespondentStatus( + 1, + 7, + ResponseStatus.IN_PROGRESS, + ), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('rejects an invalid transition', async () => { + reviews.findById.mockResolvedValue(buildReview({ id: 1 })); + respondents.getById.mockResolvedValue( + buildRespondent({ + id: 7, + reviewId: 1, + responseStatus: ResponseStatus.COMPLETED, + }), + ); + + await expect( + service.changeRespondentStatus( + 1, + 7, + ResponseStatus.IN_PROGRESS, + ), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('updates the status and emits the event for a valid transition', async () => { + reviews.findById.mockResolvedValue(buildReview({ id: 1 })); + respondents.getById.mockResolvedValue( + buildRespondent({ + id: 7, + reviewId: 1, + responseStatus: ResponseStatus.PENDING, + }), + ); + + await service.changeRespondentStatus( + 1, + 7, + ResponseStatus.IN_PROGRESS, + 42, + 'Actor', + 'reason', + ); + + expect(respondents.updateById).toHaveBeenCalledWith(7, { + responseStatus: ResponseStatus.IN_PROGRESS, + }); + const event = eventEmitter.emit.mock.calls[0][1]; + expect(event.reviewId).toBe(1); + expect(event.respondentRelationId).toBe(7); + expect(event.fromStatus).toBe(ResponseStatus.PENDING); + expect(event.toStatus).toBe(ResponseStatus.IN_PROGRESS); + expect(event.changedById).toBe(42); + expect(event.changedByName).toBe('Actor'); + expect(event.reason).toBe('reason'); + }); + }); + + describe('listRespondents', () => { + it('delegates to the respondents repository', async () => { + reviews.findById.mockResolvedValue(buildReview()); + respondents.listByReview.mockResolvedValue([buildRespondent()]); + const actor = buildUser({ roles: [IdentityRole.HR] }); + + await service.listRespondents(1, {}, actor); + + expect(respondents.listByReview).toHaveBeenCalledWith(1, {}); + }); + }); + + describe('removeRespondent', () => { + it('delegates to the repository', async () => { + await service.removeRespondent(7); + expect(respondents.deleteById).toHaveBeenCalledWith(7); + }); + }); + + describe('addReviewer', () => { + it('verifies the review exists before persisting', async () => { + reviews.findById.mockResolvedValue(buildReview()); + reviewers.create.mockImplementation(async (r: ReviewerDomain) => r); + + await service.addReviewer({ + reviewId: 1, + reviewerId: 50, + fullName: 'Reviewer', + positionId: 3, + positionTitle: 'Eng', + } as any); + + expect(reviewers.create).toHaveBeenCalledTimes(1); + }); + }); + + describe('listReviewers', () => { + it('delegates to the reviewer repository when actor has access', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + const actor = buildUser({ roles: [IdentityRole.HR] }); + reviewers.listByReview.mockResolvedValue([{ id: 1 }]); + + const result = await service.listReviewers(1, {}, actor); + expect(reviewers.listByReview).toHaveBeenCalledWith(1, {}); + expect(result).toEqual([{ id: 1 }]); + }); + + it('throws ForbiddenException when the actor has no relation to the review', async () => { + const review = buildReview(); + reviews.findById.mockResolvedValue(review); + respondents.listByReview.mockResolvedValue([]); + reviewers.listByReview.mockResolvedValue([]); + const actor = buildUser({ + id: 9999, + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.listReviewers(1, {}, actor), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('removeReviewer', () => { + it('delegates to the repository', async () => { + await service.removeReviewer(5); + expect(reviewers.deleteById).toHaveBeenCalledWith(5); + }); + }); + + describe('upsertClusterScore', () => { + it('verifies the cycle when cycleId is provided', async () => { + cycles.getById.mockResolvedValue({ id: 50 }); + clusterScores.upsert.mockImplementation( + async (s: ClusterScoreDomain) => s, + ); + + await service.upsertClusterScore({ + cycleId: 50, + clusterId: 2, + rateeId: 1, + reviewId: 1, + score: 4.5, + } as any); + + expect(cycles.getById).toHaveBeenCalledWith(50); + expect(clusterScores.upsert).toHaveBeenCalledTimes(1); + }); + + it('defaults answersCount to 1 when missing', async () => { + clusterScores.upsert.mockImplementation( + async (s: ClusterScoreDomain) => s, + ); + + const result = await service.upsertClusterScore({ + clusterId: 2, + rateeId: 1, + reviewId: 1, + score: 4.5, + } as any); + + expect(result.answersCount).toBe(1); + }); + }); + + describe('listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore', () => { + it('listClusterScores delegates to the repository', async () => { + clusterScores.list.mockResolvedValue([]); + await service.listClusterScores({}); + expect(clusterScores.list).toHaveBeenCalledWith({}); + }); + + it('getClusterScoreById delegates to the repository', async () => { + clusterScores.getById.mockResolvedValue({ id: 1 }); + await service.getClusterScoreById(1); + expect(clusterScores.getById).toHaveBeenCalledWith(1); + }); + + it('getClusterScoreByCycleId delegates to the repository', async () => { + clusterScores.getByCycleId.mockResolvedValue([]); + await service.getClusterScoreByCycleId(50); + expect(clusterScores.getByCycleId).toHaveBeenCalledWith(50); + }); + + it('removeClusterScore delegates to the repository', async () => { + await service.removeClusterScore(5); + expect(clusterScores.deleteById).toHaveBeenCalledWith(5); + }); + }); + + describe('changeReviewStage', () => { + it('rejects an invalid stage transition', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.ARCHIVED }), + ); + + await expect( + service.changeReviewStage(1, ReviewStage.NEW), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('updates the stage, creates a history record, and emits the event', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.NEW }), + ); + + await service.changeReviewStage( + 1, + ReviewStage.SELF_ASSESSMENT, + 10, + 'HR', + 'reason', + ); + + expect(txReviewUpdate).toHaveBeenCalledWith({ + where: { id: 1 }, + data: { stage: ReviewStage.SELF_ASSESSMENT }, + }); + + expect(stageHistory.create).toHaveBeenCalledTimes(1); + const history = stageHistory.create.mock.calls[0][0]; + expect(history.reviewId).toBe(1); + expect(history.fromStage).toBe(ReviewStage.NEW); + expect(history.toStage).toBe(ReviewStage.SELF_ASSESSMENT); + + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'review.stage.changed', + expect.any(ReviewStageChangedEvent), + ); + }); + }); + + describe('completeReview', () => { + it('moves the review to FINISHED when all responses are completed/canceled', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + respondents.listByReview.mockResolvedValue([ + buildRespondent({ responseStatus: ResponseStatus.COMPLETED }), + buildRespondent({ responseStatus: ResponseStatus.CANCELED }), + ]); + + await service.completeReview(1); + + expect(txReviewUpdate).toHaveBeenCalledWith({ + where: { id: 1 }, + data: { stage: ReviewStage.FINISHED }, + }); + const event = eventEmitter.emit.mock.calls[0][1]; + expect(event.reason).toBe( + REVIEW_TRANSITION_REASONS.ALL_RESPONSES_COLLECTED, + ); + }); + + it('throws when any response is still pending or in progress', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + respondents.listByReview.mockResolvedValue([ + buildRespondent({ responseStatus: ResponseStatus.PENDING }), + ]); + + await expect(service.completeReview(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + }); + + describe('forceFinishReview', () => { + it('runs changeReviewStage with the FORCE_FINISH reason', async () => { + reviews.findById.mockResolvedValue( + buildReview({ stage: ReviewStage.IN_PROGRESS }), + ); + + await service.forceFinishReview(1, 7, 'HR Admin'); + + const history = stageHistory.create.mock.calls[0][0]; + expect(history.toStage).toBe(ReviewStage.FINISHED); + expect(history.reason).toBe(REVIEW_TRANSITION_REASONS.FORCE_FINISH); + }); + }); + + describe('getStageHistory (review)', () => { + it('delegates to the stageHistory repository', async () => { + stageHistory.findByReviewId.mockResolvedValue([{ id: 1 }]); + const result = await service.getStageHistory(7); + expect(stageHistory.findByReviewId).toHaveBeenCalledWith(7); + expect(result).toEqual([{ id: 1 }]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/answer.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/answer.domain.spec.ts new file mode 100644 index 00000000..ca271a80 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/answer.domain.spec.ts @@ -0,0 +1,59 @@ +import { AnswerType, RespondentCategory } from '@intra/shared-kernel'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; + +describe('AnswerDomain', () => { + const baseProps = { + reviewId: 10, + questionId: 20, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + }; + + describe('create', () => { + it('creates a numerical answer with all properties set', () => { + const answer = AnswerDomain.create({ + id: 1, + ...baseProps, + numericalValue: 4, + textValue: null, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(answer.id).toBe(1); + expect(answer.reviewId).toBe(10); + expect(answer.questionId).toBe(20); + expect(answer.respondentCategory).toBe(RespondentCategory.TEAM); + expect(answer.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(answer.numericalValue).toBe(4); + expect(answer.textValue).toBeNull(); + expect(answer.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('creates a text answer with all properties set', () => { + const answer = AnswerDomain.create({ + ...baseProps, + answerType: AnswerType.TEXT_FIELD, + textValue: 'Great teammate', + }); + + expect(answer.answerType).toBe(AnswerType.TEXT_FIELD); + expect(answer.textValue).toBe('Great teammate'); + expect(answer.numericalValue).toBeNull(); + }); + + it('defaults numericalValue and textValue to null when missing', () => { + const answer = AnswerDomain.create(baseProps); + + expect(answer.numericalValue).toBeNull(); + expect(answer.textValue).toBeNull(); + }); + + it('omits id and createdAt when not provided', () => { + const answer = AnswerDomain.create(baseProps); + expect(answer.id).toBeUndefined(); + expect(answer.createdAt).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/cluster-score-analytics.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/cluster-score-analytics.domain.spec.ts new file mode 100644 index 00000000..e0f7e53d --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/cluster-score-analytics.domain.spec.ts @@ -0,0 +1,71 @@ +import Decimal from 'decimal.js'; +import { ClusterScoreAnalyticsDomain } from 'src/contexts/feedback360/domain/cluster-score-analytics.domain'; + +describe('ClusterScoreAnalyticsDomain', () => { + const baseProps = { + cycleId: 1, + clusterId: 2, + lowerBound: 0, + upperBound: 5, + employeesCount: 12, + employeeDensity: 0.75, + minScore: 1.5, + maxScore: 4.8, + averageScore: 3.4, + }; + + describe('create', () => { + it('creates analytics with Decimal-coerced numeric fields', () => { + const analytics = ClusterScoreAnalyticsDomain.create({ + id: 100, + ...baseProps, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(analytics.id).toBe(100); + expect(analytics.cycleId).toBe(1); + expect(analytics.clusterId).toBe(2); + expect(analytics.lowerBound).toBeInstanceOf(Decimal); + expect(analytics.upperBound).toBeInstanceOf(Decimal); + expect(analytics.employeesCount).toBeInstanceOf(Decimal); + expect(analytics.employeeDensity).toBeInstanceOf(Decimal); + expect(analytics.minScore).toBeInstanceOf(Decimal); + expect(analytics.maxScore).toBeInstanceOf(Decimal); + expect(analytics.averageScore).toBeInstanceOf(Decimal); + + expect(analytics.lowerBound.toNumber()).toBe(0); + expect(analytics.upperBound.toNumber()).toBe(5); + expect(analytics.employeesCount.toNumber()).toBe(12); + expect(analytics.employeeDensity.toNumber()).toBeCloseTo(0.75, 6); + expect(analytics.minScore.toNumber()).toBeCloseTo(1.5, 6); + expect(analytics.maxScore.toNumber()).toBeCloseTo(4.8, 6); + expect(analytics.averageScore.toNumber()).toBeCloseTo(3.4, 6); + + expect(analytics.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(analytics.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('accepts string numeric inputs and Decimal instances', () => { + const analytics = ClusterScoreAnalyticsDomain.create({ + ...baseProps, + lowerBound: '1.2', + upperBound: new Decimal('4.4'), + }); + + expect(analytics.lowerBound.toString()).toBe('1.2'); + expect(analytics.upperBound.toString()).toBe('4.4'); + }); + + it('omits id, createdAt and updatedAt when not provided', () => { + const analytics = ClusterScoreAnalyticsDomain.create(baseProps); + expect(analytics.id).toBeUndefined(); + expect(analytics.createdAt).toBeUndefined(); + expect(analytics.updatedAt).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/cluster-score-with-relations.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/cluster-score-with-relations.domain.spec.ts new file mode 100644 index 00000000..246ede49 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/cluster-score-with-relations.domain.spec.ts @@ -0,0 +1,67 @@ +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { ClusterScoreWithRelationsDomain } from 'src/contexts/feedback360/domain/cluster-score-with-relations.domain'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; + +describe('ClusterScoreWithRelationsDomain', () => { + const cluster = ClusterDomain.create({ + id: 1, + competenceId: 2, + lowerBound: 0, + upperBound: 5, + title: 'Soft Skills', + description: 'Communication & teamwork', + }); + + const ratee = UserDomain.create({ + id: 42, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + roles: [IdentityRole.EMPLOYEE], + }); + + describe('create', () => { + it('builds the aggregate with cluster, ratee, and score relations', () => { + const aggregate = ClusterScoreWithRelationsDomain.create({ + id: 10, + cycleId: 100, + clusterId: cluster.id!, + rateeId: ratee.id!, + reviewId: 5, + score: 3.14, + answersCount: 9, + cluster, + ratee, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(aggregate.id).toBe(10); + expect(aggregate.cycleId).toBe(100); + expect(aggregate.clusterId).toBe(cluster.id); + expect(aggregate.rateeId).toBe(ratee.id); + expect(aggregate.reviewId).toBe(5); + expect(aggregate.score).toBeInstanceOf(Decimal); + expect(aggregate.score.toNumber()).toBeCloseTo(3.14, 6); + expect(aggregate.answersCount).toBe(9); + expect(aggregate.cluster).toBe(cluster); + expect(aggregate.ratee).toBe(ratee); + }); + + it('defaults the cycleId to null when not provided', () => { + const aggregate = ClusterScoreWithRelationsDomain.create({ + clusterId: 1, + rateeId: 1, + reviewId: 1, + score: 1, + answersCount: 1, + cluster, + ratee, + }); + expect(aggregate.cycleId).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/cluster-score.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/cluster-score.domain.spec.ts new file mode 100644 index 00000000..91f92c5d --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/cluster-score.domain.spec.ts @@ -0,0 +1,61 @@ +import Decimal from 'decimal.js'; +import { ClusterScoreDomain } from 'src/contexts/feedback360/domain/cluster-score.domain'; + +describe('ClusterScoreDomain', () => { + const baseProps = { + clusterId: 1, + rateeId: 2, + reviewId: 3, + score: 4.25, + answersCount: 6, + }; + + describe('create', () => { + it('creates a cluster score with all supplied properties', () => { + const score = ClusterScoreDomain.create({ + id: 10, + ...baseProps, + cycleId: 100, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(score.id).toBe(10); + expect(score.cycleId).toBe(100); + expect(score.clusterId).toBe(1); + expect(score.rateeId).toBe(2); + expect(score.reviewId).toBe(3); + expect(score.score).toBeInstanceOf(Decimal); + expect(score.score.toNumber()).toBeCloseTo(4.25, 6); + expect(score.answersCount).toBe(6); + expect(score.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(score.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('coerces a numeric score input into a Decimal instance', () => { + const score = ClusterScoreDomain.create({ + ...baseProps, + score: 3, + }); + expect(score.score).toBeInstanceOf(Decimal); + expect(score.score.equals(new Decimal(3))).toBe(true); + }); + + it('accepts a string score input', () => { + const score = ClusterScoreDomain.create({ + ...baseProps, + score: '2.5', + }); + expect(score.score.toString()).toBe('2.5'); + }); + + it('defaults the cycleId to null when not provided', () => { + const score = ClusterScoreDomain.create(baseProps); + expect(score.cycleId).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/cycle-stage-history.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/cycle-stage-history.domain.spec.ts new file mode 100644 index 00000000..c62b7030 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/cycle-stage-history.domain.spec.ts @@ -0,0 +1,41 @@ +import { CycleStage } from '@intra/shared-kernel'; +import { CycleStageHistoryDomain } from 'src/contexts/feedback360/domain/cycle-stage-history.domain'; + +describe('CycleStageHistoryDomain', () => { + const baseProps = { + cycleId: 1, + fromStage: CycleStage.NEW, + toStage: CycleStage.ACTIVE, + }; + + describe('create', () => { + it('creates a history record with all supplied properties', () => { + const history = CycleStageHistoryDomain.create({ + id: 99, + ...baseProps, + changedById: 7, + changedByName: 'HR Manager', + reason: 'Cycle updated', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(history.id).toBe(99); + expect(history.cycleId).toBe(1); + expect(history.fromStage).toBe(CycleStage.NEW); + expect(history.toStage).toBe(CycleStage.ACTIVE); + expect(history.changedById).toBe(7); + expect(history.changedByName).toBe('HR Manager'); + expect(history.reason).toBe('Cycle updated'); + expect(history.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('normalises missing actor/reason fields to null', () => { + const history = CycleStageHistoryDomain.create(baseProps); + expect(history.changedById).toBeNull(); + expect(history.changedByName).toBeNull(); + expect(history.reason).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/cycle.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/cycle.domain.spec.ts new file mode 100644 index 00000000..0b2c2e8e --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/cycle.domain.spec.ts @@ -0,0 +1,101 @@ +import { CYCLE_CONSTRAINTS, CycleStage } from '@intra/shared-kernel'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; + +describe('CycleDomain', () => { + const baseProps = { + title: 'Q1 2025', + hrId: 7, + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-03-31T00:00:00.000Z'), + }; + + describe('create', () => { + it('creates a cycle with the supplied properties', () => { + const cycle = CycleDomain.create({ + id: 1, + ...baseProps, + description: 'First cycle', + stage: CycleStage.ACTIVE, + isActive: true, + minRespondentsThreshold: 8, + reviewDeadline: new Date('2025-02-15T00:00:00.000Z'), + approvalDeadline: new Date('2025-02-25T00:00:00.000Z'), + responseDeadline: new Date('2025-03-15T00:00:00.000Z'), + createdAt: new Date('2024-12-31T00:00:00.000Z'), + updatedAt: new Date('2024-12-31T01:00:00.000Z'), + }); + + expect(cycle.id).toBe(1); + expect(cycle.title).toBe('Q1 2025'); + expect(cycle.description).toBe('First cycle'); + expect(cycle.hrId).toBe(7); + expect(cycle.stage).toBe(CycleStage.ACTIVE); + expect(cycle.isActive).toBe(true); + expect(cycle.minRespondentsThreshold).toBe(8); + expect(cycle.startDate).toEqual(baseProps.startDate); + expect(cycle.endDate).toEqual(baseProps.endDate); + expect(cycle.reviewDeadline).toEqual( + new Date('2025-02-15T00:00:00.000Z'), + ); + expect(cycle.approvalDeadline).toEqual( + new Date('2025-02-25T00:00:00.000Z'), + ); + expect(cycle.responseDeadline).toEqual( + new Date('2025-03-15T00:00:00.000Z'), + ); + expect(cycle.createdAt).toEqual( + new Date('2024-12-31T00:00:00.000Z'), + ); + expect(cycle.updatedAt).toEqual( + new Date('2024-12-31T01:00:00.000Z'), + ); + }); + + it('defaults the stage to NEW when not provided', () => { + const cycle = CycleDomain.create(baseProps); + expect(cycle.stage).toBe(CycleStage.NEW); + }); + + it('defaults isActive to true when not provided', () => { + const cycle = CycleDomain.create(baseProps); + expect(cycle.isActive).toBe(true); + }); + + it('defaults minRespondentsThreshold to the anonymity threshold minimum', () => { + const cycle = CycleDomain.create(baseProps); + expect(cycle.minRespondentsThreshold).toBe( + CYCLE_CONSTRAINTS.ANONYMITY_THRESHOLD.MIN, + ); + }); + + it('normalises missing description and deadlines to null', () => { + const cycle = CycleDomain.create(baseProps); + expect(cycle.description).toBeNull(); + expect(cycle.reviewDeadline).toBeNull(); + expect(cycle.approvalDeadline).toBeNull(); + expect(cycle.responseDeadline).toBeNull(); + }); + + it('preserves explicit null values for optional fields', () => { + const cycle = CycleDomain.create({ + ...baseProps, + description: null, + reviewDeadline: null, + approvalDeadline: null, + responseDeadline: null, + }); + + expect(cycle.description).toBeNull(); + expect(cycle.reviewDeadline).toBeNull(); + expect(cycle.approvalDeadline).toBeNull(); + expect(cycle.responseDeadline).toBeNull(); + }); + + it('omits id, createdAt and updatedAt when not provided', () => { + const cycle = CycleDomain.create(baseProps); + expect(cycle.id).toBeUndefined(); + expect(cycle.createdAt).toBeUndefined(); + expect(cycle.updatedAt).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/question.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/question.domain.spec.ts new file mode 100644 index 00000000..3bd930ea --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/question.domain.spec.ts @@ -0,0 +1,54 @@ +import { AnswerType } from '@intra/shared-kernel'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; + +describe('QuestionDomain', () => { + const baseProps = { + title: 'Are you a team player?', + answerType: AnswerType.NUMERICAL_SCALE, + }; + + describe('create', () => { + it('creates a question with all supplied properties', () => { + const question = QuestionDomain.create({ + id: 1, + ...baseProps, + cycleId: 100, + questionTemplateId: 50, + competenceId: 7, + isForSelfassessment: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(question.id).toBe(1); + expect(question.title).toBe('Are you a team player?'); + expect(question.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(question.cycleId).toBe(100); + expect(question.questionTemplateId).toBe(50); + expect(question.competenceId).toBe(7); + expect(question.isForSelfassessment).toBe(true); + expect(question.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('normalises missing optional foreign keys to null', () => { + const question = QuestionDomain.create(baseProps); + expect(question.cycleId).toBeNull(); + expect(question.questionTemplateId).toBeNull(); + expect(question.competenceId).toBeNull(); + }); + + it('defaults isForSelfassessment to false when not provided', () => { + const question = QuestionDomain.create(baseProps); + expect(question.isForSelfassessment).toBe(false); + }); + + it('coerces null isForSelfassessment to false', () => { + const question = QuestionDomain.create({ + ...baseProps, + isForSelfassessment: null, + }); + expect(question.isForSelfassessment).toBe(false); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/respondent.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/respondent.domain.spec.ts new file mode 100644 index 00000000..13d051b2 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/respondent.domain.spec.ts @@ -0,0 +1,69 @@ +import { RespondentCategory, ResponseStatus } from '@intra/shared-kernel'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; + +describe('RespondentDomain', () => { + const baseProps = { + reviewId: 1, + respondentId: 2, + fullName: 'John Smith', + category: RespondentCategory.TEAM, + positionId: 3, + positionTitle: 'Engineer', + }; + + describe('create', () => { + it('creates a respondent with all supplied properties', () => { + const respondent = RespondentDomain.create({ + id: 1, + ...baseProps, + responseStatus: ResponseStatus.IN_PROGRESS, + respondentNote: 'feedback note', + hrNote: 'hr note', + teamId: 5, + teamTitle: 'Team A', + invitedAt: new Date('2025-01-01T00:00:00.000Z'), + canceledAt: new Date('2025-01-02T00:00:00.000Z'), + respondedAt: new Date('2025-01-03T00:00:00.000Z'), + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-04T00:00:00.000Z'), + }); + + expect(respondent.id).toBe(1); + expect(respondent.reviewId).toBe(1); + expect(respondent.respondentId).toBe(2); + expect(respondent.fullName).toBe('John Smith'); + expect(respondent.category).toBe(RespondentCategory.TEAM); + expect(respondent.responseStatus).toBe(ResponseStatus.IN_PROGRESS); + expect(respondent.respondentNote).toBe('feedback note'); + expect(respondent.hrNote).toBe('hr note'); + expect(respondent.teamId).toBe(5); + expect(respondent.teamTitle).toBe('Team A'); + expect(respondent.invitedAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(respondent.canceledAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + expect(respondent.respondedAt).toEqual( + new Date('2025-01-03T00:00:00.000Z'), + ); + }); + + it('defaults responseStatus to PENDING when not provided', () => { + const respondent = RespondentDomain.create(baseProps); + expect(respondent.responseStatus).toBe(ResponseStatus.PENDING); + }); + + it('normalises missing optional fields to null', () => { + const respondent = RespondentDomain.create(baseProps); + + expect(respondent.respondentNote).toBeNull(); + expect(respondent.hrNote).toBeNull(); + expect(respondent.teamId).toBeNull(); + expect(respondent.teamTitle).toBeNull(); + expect(respondent.invitedAt).toBeNull(); + expect(respondent.canceledAt).toBeNull(); + expect(respondent.respondedAt).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/review-question-relation.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/review-question-relation.domain.spec.ts new file mode 100644 index 00000000..8f3f8444 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/review-question-relation.domain.spec.ts @@ -0,0 +1,51 @@ +import { AnswerType } from '@intra/shared-kernel'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; + +describe('ReviewQuestionRelationDomain', () => { + const baseProps = { + reviewId: 1, + questionId: 2, + questionTitle: 'How effective is teamwork?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 3, + competenceTitle: 'Teamwork', + }; + + describe('create', () => { + it('creates a relation with all supplied properties', () => { + const relation = ReviewQuestionRelationDomain.create({ + id: 10, + ...baseProps, + competenceCode: 'TWK', + competenceDescription: 'Working with peers', + isForSelfassessment: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(relation.id).toBe(10); + expect(relation.reviewId).toBe(1); + expect(relation.questionId).toBe(2); + expect(relation.questionTitle).toBe('How effective is teamwork?'); + expect(relation.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(relation.competenceId).toBe(3); + expect(relation.competenceTitle).toBe('Teamwork'); + expect(relation.competenceCode).toBe('TWK'); + expect(relation.competenceDescription).toBe('Working with peers'); + expect(relation.isForSelfassessment).toBe(true); + expect(relation.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('normalises missing competence code/description to null', () => { + const relation = ReviewQuestionRelationDomain.create(baseProps); + expect(relation.competenceCode).toBeNull(); + expect(relation.competenceDescription).toBeNull(); + }); + + it('defaults isForSelfassessment to false when not provided', () => { + const relation = ReviewQuestionRelationDomain.create(baseProps); + expect(relation.isForSelfassessment).toBe(false); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/review-stage-history.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/review-stage-history.domain.spec.ts new file mode 100644 index 00000000..8b40b5b8 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/review-stage-history.domain.spec.ts @@ -0,0 +1,41 @@ +import { ReviewStage } from '@intra/shared-kernel'; +import { ReviewStageHistoryDomain } from 'src/contexts/feedback360/domain/review-stage-history.domain'; + +describe('ReviewStageHistoryDomain', () => { + const baseProps = { + reviewId: 1, + fromStage: ReviewStage.NEW, + toStage: ReviewStage.SELF_ASSESSMENT, + }; + + describe('create', () => { + it('creates a history record with all supplied properties', () => { + const history = ReviewStageHistoryDomain.create({ + id: 11, + ...baseProps, + changedById: 8, + changedByName: 'HR Manager', + reason: 'Review updated', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(history.id).toBe(11); + expect(history.reviewId).toBe(1); + expect(history.fromStage).toBe(ReviewStage.NEW); + expect(history.toStage).toBe(ReviewStage.SELF_ASSESSMENT); + expect(history.changedById).toBe(8); + expect(history.changedByName).toBe('HR Manager'); + expect(history.reason).toBe('Review updated'); + expect(history.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('normalises missing actor/reason fields to null', () => { + const history = ReviewStageHistoryDomain.create(baseProps); + expect(history.changedById).toBeNull(); + expect(history.changedByName).toBeNull(); + expect(history.reason).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/review.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/review.domain.spec.ts new file mode 100644 index 00000000..c275ddec --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/review.domain.spec.ts @@ -0,0 +1,96 @@ +import { ReviewStage } from '@intra/shared-kernel'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; + +describe('ReviewDomain', () => { + const baseProps = { + rateeId: 11, + rateeFullName: 'Jane Doe', + rateePositionId: 3, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR Manager', + }; + + describe('create', () => { + it('creates a review with all supplied properties', () => { + const review = ReviewDomain.create({ + id: 100, + ...baseProps, + hrNote: 'note', + teamId: 4, + teamTitle: 'Team A', + managerId: 5, + managerFullName: 'Manager Name', + managerPositionId: 6, + managerPositionTitle: 'Engineering Lead', + cycleId: 50, + stage: ReviewStage.IN_PROGRESS, + reportId: 99, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-01T01:00:00.000Z'), + }); + + expect(review.id).toBe(100); + expect(review.rateeId).toBe(11); + expect(review.rateeFullName).toBe('Jane Doe'); + expect(review.rateePositionId).toBe(3); + expect(review.rateePositionTitle).toBe('Engineer'); + expect(review.hrId).toBe(2); + expect(review.hrFullName).toBe('HR Manager'); + expect(review.hrNote).toBe('note'); + expect(review.teamId).toBe(4); + expect(review.teamTitle).toBe('Team A'); + expect(review.managerId).toBe(5); + expect(review.managerFullName).toBe('Manager Name'); + expect(review.managerPositionId).toBe(6); + expect(review.managerPositionTitle).toBe('Engineering Lead'); + expect(review.cycleId).toBe(50); + expect(review.stage).toBe(ReviewStage.IN_PROGRESS); + expect(review.reportId).toBe(99); + expect(review.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(review.updatedAt).toEqual( + new Date('2025-01-01T01:00:00.000Z'), + ); + }); + + it('defaults the stage to NEW when not provided', () => { + const review = ReviewDomain.create(baseProps); + expect(review.stage).toBe(ReviewStage.NEW); + }); + + it('normalises missing optional fields to null', () => { + const review = ReviewDomain.create(baseProps); + expect(review.hrNote).toBeNull(); + expect(review.teamId).toBeNull(); + expect(review.teamTitle).toBeNull(); + expect(review.managerId).toBeNull(); + expect(review.managerFullName).toBeNull(); + expect(review.managerPositionId).toBeNull(); + expect(review.managerPositionTitle).toBeNull(); + expect(review.cycleId).toBeNull(); + expect(review.reportId).toBeNull(); + }); + + it('preserves explicit null values for optional fields', () => { + const review = ReviewDomain.create({ + ...baseProps, + hrNote: null, + teamId: null, + teamTitle: null, + managerId: null, + managerFullName: null, + managerPositionId: null, + managerPositionTitle: null, + cycleId: null, + reportId: null, + }); + + expect(review.hrNote).toBeNull(); + expect(review.cycleId).toBeNull(); + expect(review.reportId).toBeNull(); + expect(review.managerId).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/domain/reviewer.domain.spec.ts b/apps/api/test/unit/contexts/feedback360/domain/reviewer.domain.spec.ts new file mode 100644 index 00000000..6c4a9ba5 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/domain/reviewer.domain.spec.ts @@ -0,0 +1,41 @@ +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; + +describe('ReviewerDomain', () => { + const baseProps = { + reviewId: 10, + reviewerId: 20, + fullName: 'Reviewer One', + positionId: 3, + positionTitle: 'Senior Engineer', + }; + + describe('create', () => { + it('creates a reviewer with all supplied properties', () => { + const reviewer = ReviewerDomain.create({ + id: 1, + ...baseProps, + teamId: 4, + teamTitle: 'Team B', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(reviewer.id).toBe(1); + expect(reviewer.reviewId).toBe(10); + expect(reviewer.reviewerId).toBe(20); + expect(reviewer.fullName).toBe('Reviewer One'); + expect(reviewer.positionId).toBe(3); + expect(reviewer.positionTitle).toBe('Senior Engineer'); + expect(reviewer.teamId).toBe(4); + expect(reviewer.teamTitle).toBe('Team B'); + expect(reviewer.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('normalises missing team fields to null', () => { + const reviewer = ReviewerDomain.create(baseProps); + expect(reviewer.teamId).toBeNull(); + expect(reviewer.teamTitle).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/answer.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/answer.mapper.spec.ts new file mode 100644 index 00000000..28c93a6b --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/answer.mapper.spec.ts @@ -0,0 +1,66 @@ +import { Answer as PrismaAnswer } from '@intra/database'; +import { AnswerType, RespondentCategory } from '@intra/shared-kernel'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { AnswerMapper } from 'src/contexts/feedback360/infrastructure/mappers/answer.mapper'; + +describe('AnswerMapper', () => { + const prismaAnswer = { + id: 1, + reviewId: 10, + questionId: 20, + respondentCategory: 'TEAM' as PrismaAnswer['respondentCategory'], + answerType: 'NUMERICAL_SCALE' as PrismaAnswer['answerType'], + numericalValue: 5, + textValue: null, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaAnswer; + + describe('toDomain', () => { + it('converts a prisma row into an AnswerDomain instance', () => { + const domain = AnswerMapper.toDomain(prismaAnswer); + + expect(domain).toBeInstanceOf(AnswerDomain); + expect(domain.id).toBe(1); + expect(domain.reviewId).toBe(10); + expect(domain.questionId).toBe(20); + expect(domain.respondentCategory).toBe(RespondentCategory.TEAM); + expect(domain.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(domain.numericalValue).toBe(5); + expect(domain.textValue).toBeNull(); + }); + }); + + describe('toPrisma', () => { + it('serialises an AnswerDomain into a Prisma create input', () => { + const domain = AnswerDomain.create({ + reviewId: 10, + questionId: 20, + respondentCategory: RespondentCategory.SELF_ASSESSMENT, + answerType: AnswerType.TEXT_FIELD, + textValue: 'Great work', + }); + + const prisma = AnswerMapper.toPrisma(domain); + + expect(prisma.reviewId).toBe(10); + expect(prisma.questionId).toBe(20); + expect(prisma.respondentCategory).toBe('SELF_ASSESSMENT'); + expect(prisma.answerType).toBe('TEXT_FIELD'); + expect(prisma.textValue).toBe('Great work'); + expect(prisma.numericalValue).toBeNull(); + }); + }); + + describe('category conversions', () => { + it('uppercases the category in both directions', () => { + expect(AnswerMapper.toPrismaCategory(RespondentCategory.TEAM)).toBe( + 'TEAM', + ); + expect( + AnswerMapper.fromPrismaCategory( + 'OTHER' as PrismaAnswer['respondentCategory'], + ), + ).toBe(RespondentCategory.OTHER); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cluster-score-analytics.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cluster-score-analytics.mapper.spec.ts new file mode 100644 index 00000000..87b61334 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cluster-score-analytics.mapper.spec.ts @@ -0,0 +1,67 @@ +import { ClusterScoreAnalytics as PrismaClusterScoreAnalytics } from '@intra/database'; +import Decimal from 'decimal.js'; +import { ClusterScoreAnalyticsDomain } from 'src/contexts/feedback360/domain/cluster-score-analytics.domain'; +import { ClusterScoreAnalyticsMapper } from 'src/contexts/feedback360/infrastructure/mappers/cluster-score-analytics.mapper'; + +describe('ClusterScoreAnalyticsMapper', () => { + const prismaRow = { + id: 1, + cycleId: 100, + clusterId: 2, + employeesCount: '12.0000', + employeeDensity: '0.7500', + lowerBound: '0.0000', + upperBound: '5.0000', + minScore: '1.5000', + maxScore: '4.8000', + averageScore: '3.4000', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as unknown as PrismaClusterScoreAnalytics; + + describe('toDomain', () => { + it('converts the row to a ClusterScoreAnalyticsDomain with Decimal values', () => { + const domain = ClusterScoreAnalyticsMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(ClusterScoreAnalyticsDomain); + expect(domain.id).toBe(1); + expect(domain.cycleId).toBe(100); + expect(domain.clusterId).toBe(2); + expect(domain.employeesCount.toNumber()).toBe(12); + expect(domain.employeeDensity.toNumber()).toBeCloseTo(0.75, 6); + expect(domain.lowerBound.toNumber()).toBe(0); + expect(domain.upperBound.toNumber()).toBe(5); + expect(domain.minScore.toNumber()).toBeCloseTo(1.5, 6); + expect(domain.maxScore.toNumber()).toBeCloseTo(4.8, 6); + expect(domain.averageScore.toNumber()).toBeCloseTo(3.4, 6); + }); + }); + + describe('toPrisma', () => { + it('renders all numeric fields as 4-decimal-place strings', () => { + const domain = ClusterScoreAnalyticsDomain.create({ + cycleId: 100, + clusterId: 2, + employeesCount: new Decimal(12), + employeeDensity: new Decimal(0.75), + lowerBound: new Decimal(0), + upperBound: new Decimal(5), + minScore: new Decimal(1.5), + maxScore: new Decimal(4.8), + averageScore: new Decimal(3.4), + }); + + const prisma = ClusterScoreAnalyticsMapper.toPrisma(domain); + + expect(prisma.cycleId).toBe(100); + expect(prisma.clusterId).toBe(2); + expect(prisma.employeesCount).toBe('12.0000'); + expect(prisma.employeeDensity).toBe('0.7500'); + expect(prisma.lowerBound).toBe('0.0000'); + expect(prisma.upperBound).toBe('5.0000'); + expect(prisma.minScore).toBe('1.5000'); + expect(prisma.maxScore).toBe('4.8000'); + expect(prisma.averageScore).toBe('3.4000'); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cluster-score.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cluster-score.mapper.spec.ts new file mode 100644 index 00000000..28f3d173 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cluster-score.mapper.spec.ts @@ -0,0 +1,118 @@ +import { + Cluster as PrismaCluster, + ClusterScore as PrismaClusterScore, + User as PrismaUser, +} from '@intra/database'; +import Decimal from 'decimal.js'; +import { ClusterScoreWithRelationsDomain } from 'src/contexts/feedback360/domain/cluster-score-with-relations.domain'; +import { ClusterScoreDomain } from 'src/contexts/feedback360/domain/cluster-score.domain'; +import { ClusterScoreMapper } from 'src/contexts/feedback360/infrastructure/mappers/cluster-score.mapper'; + +describe('ClusterScoreMapper', () => { + const prismaClusterScore = { + id: 1, + cycleId: 100, + clusterId: 2, + rateeId: 11, + reviewId: 5, + score: '3.5000', + answersCount: 4, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as unknown as PrismaClusterScore; + + describe('toDomain', () => { + it('converts a prisma row into a ClusterScoreDomain instance', () => { + const domain = ClusterScoreMapper.toDomain(prismaClusterScore); + + expect(domain).toBeInstanceOf(ClusterScoreDomain); + expect(domain.id).toBe(1); + expect(domain.cycleId).toBe(100); + expect(domain.clusterId).toBe(2); + expect(domain.rateeId).toBe(11); + expect(domain.reviewId).toBe(5); + expect(domain.score).toBeInstanceOf(Decimal); + expect(domain.score.toNumber()).toBe(3.5); + expect(domain.answersCount).toBe(4); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input and rounds the score to 4dp', () => { + const domain = ClusterScoreDomain.create({ + cycleId: 100, + clusterId: 2, + rateeId: 11, + reviewId: 5, + score: 3.456789, + answersCount: 4, + }); + + const prisma = ClusterScoreMapper.toPrisma(domain); + + expect(prisma.cycleId).toBe(100); + expect(prisma.clusterId).toBe(2); + expect(prisma.score).toBe('3.4568'); + expect(prisma.answersCount).toBe(4); + }); + }); + + describe('toDomainWithRelations', () => { + const prismaCluster = { + id: 2, + competenceId: 7, + lowerBound: '0', + upperBound: '5', + title: 'Soft Skills', + description: 'Communication & teamwork', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-01T00:00:00.000Z'), + } as unknown as PrismaCluster; + + const prismaUser = { + id: 11, + firstName: 'Jane', + secondName: null, + lastName: 'Doe', + fullName: 'Jane Doe', + email: 'jane@example.com', + avatarUrl: null, + status: 'ACTIVE' as PrismaUser['status'], + positionId: 3, + teamId: 4, + managerId: 5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-01T00:00:00.000Z'), + } as unknown as PrismaUser; + + it('returns the full aggregate including cluster and ratee', () => { + const aggregate = ClusterScoreMapper.toDomainWithRelations({ + ...prismaClusterScore, + cluster: prismaCluster, + ratee: prismaUser, + }); + + expect(aggregate).toBeInstanceOf(ClusterScoreWithRelationsDomain); + expect(aggregate.id).toBe(1); + expect(aggregate.cluster.id).toBe(2); + expect(aggregate.cluster.title).toBe('Soft Skills'); + expect(aggregate.cluster.lowerBound).toBe(0); + expect(aggregate.cluster.upperBound).toBe(5); + expect(aggregate.ratee.id).toBe(11); + expect(aggregate.ratee.fullName).toBe('Jane Doe'); + expect(aggregate.ratee.email).toBe('jane@example.com'); + }); + }); + + describe('toScoreDecimalString', () => { + it('rounds to exactly 4 decimal places', () => { + expect(ClusterScoreMapper.toScoreDecimalString(3.45678)).toBe( + '3.4568', + ); + expect(ClusterScoreMapper.toScoreDecimalString(3)).toBe('3.0000'); + expect(ClusterScoreMapper.toScoreDecimalString('2.1')).toBe( + '2.1000', + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cycle-stage-history.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cycle-stage-history.mapper.spec.ts new file mode 100644 index 00000000..f6f353aa --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cycle-stage-history.mapper.spec.ts @@ -0,0 +1,53 @@ +import { CycleStageHistory as PrismaCycleStageHistory } from '@intra/database'; +import { CycleStage } from '@intra/shared-kernel'; +import { CycleStageHistoryDomain } from 'src/contexts/feedback360/domain/cycle-stage-history.domain'; +import { CycleStageHistoryMapper } from 'src/contexts/feedback360/infrastructure/mappers/cycle-stage-history.mapper'; + +describe('CycleStageHistoryMapper', () => { + const prismaHistory = { + id: 11, + cycleId: 100, + fromStage: 'NEW' as PrismaCycleStageHistory['fromStage'], + toStage: 'ACTIVE' as PrismaCycleStageHistory['toStage'], + changedById: 7, + changedByName: 'HR Manager', + reason: 'Cycle updated', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaCycleStageHistory; + + describe('toDomain', () => { + it('converts a prisma row into a CycleStageHistoryDomain', () => { + const domain = CycleStageHistoryMapper.toDomain(prismaHistory); + + expect(domain).toBeInstanceOf(CycleStageHistoryDomain); + expect(domain.id).toBe(11); + expect(domain.cycleId).toBe(100); + expect(domain.fromStage).toBe(CycleStage.NEW); + expect(domain.toStage).toBe(CycleStage.ACTIVE); + expect(domain.changedById).toBe(7); + expect(domain.changedByName).toBe('HR Manager'); + expect(domain.reason).toBe('Cycle updated'); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input', () => { + const domain = CycleStageHistoryDomain.create({ + cycleId: 100, + fromStage: CycleStage.ACTIVE, + toStage: CycleStage.FINISHED, + changedById: 0, + changedByName: 'System', + }); + + const prisma = CycleStageHistoryMapper.toPrisma(domain); + + expect(prisma.cycleId).toBe(100); + expect(prisma.fromStage).toBe('ACTIVE'); + expect(prisma.toStage).toBe('FINISHED'); + expect(prisma.changedById).toBe(0); + expect(prisma.changedByName).toBe('System'); + expect(prisma.reason).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cycle.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cycle.mapper.spec.ts new file mode 100644 index 00000000..ddcfbdda --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cycle.mapper.spec.ts @@ -0,0 +1,99 @@ +import { Cycle as PrismaCycle } from '@intra/database'; +import { CycleStage } from '@intra/shared-kernel'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { CycleMapper } from 'src/contexts/feedback360/infrastructure/mappers/cycle.mapper'; + +describe('CycleMapper', () => { + const prismaCycle: PrismaCycle = { + id: 1, + title: 'Q1 2025', + description: 'First cycle', + hrId: 7, + minRespondentsThreshold: 5, + stage: 'NEW' as PrismaCycle['stage'], + isActive: true, + startDate: new Date('2025-01-01T00:00:00.000Z'), + reviewDeadline: new Date('2025-02-01T00:00:00.000Z'), + approvalDeadline: new Date('2025-02-15T00:00:00.000Z'), + responseDeadline: new Date('2025-03-01T00:00:00.000Z'), + endDate: new Date('2025-03-31T00:00:00.000Z'), + createdAt: new Date('2024-12-31T00:00:00.000Z'), + updatedAt: new Date('2024-12-31T01:00:00.000Z'), + } as PrismaCycle; + + describe('toDomain', () => { + it('converts a prisma row into a CycleDomain instance', () => { + const domain = CycleMapper.toDomain(prismaCycle); + + expect(domain).toBeInstanceOf(CycleDomain); + expect(domain.id).toBe(1); + expect(domain.title).toBe('Q1 2025'); + expect(domain.description).toBe('First cycle'); + expect(domain.hrId).toBe(7); + expect(domain.minRespondentsThreshold).toBe(5); + expect(domain.stage).toBe(CycleStage.NEW); + expect(domain.isActive).toBe(true); + expect(domain.startDate).toEqual(prismaCycle.startDate); + expect(domain.endDate).toEqual(prismaCycle.endDate); + }); + + it('treats null isActive as true', () => { + const domain = CycleMapper.toDomain({ + ...prismaCycle, + isActive: null, + } as unknown as PrismaCycle); + expect(domain.isActive).toBe(true); + }); + }); + + describe('toPrisma', () => { + it('serialises a CycleDomain into a Prisma create input', () => { + const domain = CycleDomain.create({ + title: 'Q2 2025', + description: null, + hrId: 3, + minRespondentsThreshold: 6, + stage: CycleStage.ACTIVE, + isActive: false, + startDate: new Date('2025-04-01T00:00:00.000Z'), + reviewDeadline: null, + approvalDeadline: null, + responseDeadline: null, + endDate: new Date('2025-06-30T00:00:00.000Z'), + }); + + const prisma = CycleMapper.toPrisma(domain); + + expect(prisma).toEqual({ + title: 'Q2 2025', + description: null, + hrId: 3, + minRespondentsThreshold: 6, + stage: 'ACTIVE', + isActive: false, + startDate: new Date('2025-04-01T00:00:00.000Z'), + reviewDeadline: null, + approvalDeadline: null, + responseDeadline: null, + endDate: new Date('2025-06-30T00:00:00.000Z'), + }); + }); + }); + + describe('stage conversions', () => { + it('uppercases the stage when going to Prisma', () => { + expect(CycleMapper.toPrismaStage(CycleStage.NEW)).toBe('NEW'); + expect(CycleMapper.toPrismaStage(CycleStage.ARCHIVED)).toBe( + 'ARCHIVED', + ); + }); + + it('uppercases the stage when coming from Prisma', () => { + expect( + CycleMapper.fromPrismaStage( + 'PREPARING_REPORT' as PrismaCycle['stage'], + ), + ).toBe(CycleStage.PREPARING_REPORT); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/question.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/question.mapper.spec.ts new file mode 100644 index 00000000..b51646b0 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/question.mapper.spec.ts @@ -0,0 +1,73 @@ +import { Question as PrismaQuestion } from '@intra/database'; +import { AnswerType } from '@intra/shared-kernel'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { QuestionMapper } from 'src/contexts/feedback360/infrastructure/mappers/question.mapper'; + +describe('QuestionMapper', () => { + const prismaQuestion = { + id: 1, + cycleId: 100, + questionTemplateId: 50, + title: 'Question?', + answerType: 'NUMERICAL_SCALE' as PrismaQuestion['answerType'], + competenceId: 7, + isForSelfassessment: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaQuestion; + + describe('toDomain', () => { + it('converts a prisma row into a QuestionDomain instance', () => { + const domain = QuestionMapper.toDomain(prismaQuestion); + + expect(domain).toBeInstanceOf(QuestionDomain); + expect(domain.id).toBe(1); + expect(domain.title).toBe('Question?'); + expect(domain.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(domain.competenceId).toBe(7); + expect(domain.cycleId).toBe(100); + expect(domain.questionTemplateId).toBe(50); + expect(domain.isForSelfassessment).toBe(true); + }); + + it('falls back isForSelfassessment to false when null', () => { + const domain = QuestionMapper.toDomain({ + ...prismaQuestion, + isForSelfassessment: null, + } as unknown as PrismaQuestion); + expect(domain.isForSelfassessment).toBe(false); + }); + }); + + describe('toPrisma', () => { + it('serialises a QuestionDomain into a Prisma create input', () => { + const domain = QuestionDomain.create({ + cycleId: 100, + title: 'New question?', + answerType: AnswerType.TEXT_FIELD, + competenceId: 7, + isForSelfassessment: false, + }); + + const prisma = QuestionMapper.toPrisma(domain); + + expect(prisma.cycleId).toBe(100); + expect(prisma.title).toBe('New question?'); + expect(prisma.answerType).toBe('TEXT_FIELD'); + expect(prisma.competenceId).toBe(7); + expect(prisma.isForSelfassessment).toBe(false); + }); + }); + + describe('answer type conversions', () => { + it('uppercases the answer type in both directions', () => { + expect( + QuestionMapper.toPrismaAnswerType(AnswerType.NUMERICAL_SCALE), + ).toBe('NUMERICAL_SCALE'); + expect( + QuestionMapper.fromPrismaAnswerType( + 'TEXT_FIELD' as PrismaQuestion['answerType'], + ), + ).toBe(AnswerType.TEXT_FIELD); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/respondent.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/respondent.mapper.spec.ts new file mode 100644 index 00000000..102e21ea --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/respondent.mapper.spec.ts @@ -0,0 +1,92 @@ +import { Respondent as PrismaRespondent } from '@intra/database'; +import { RespondentCategory, ResponseStatus } from '@intra/shared-kernel'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { RespondentMapper } from 'src/contexts/feedback360/infrastructure/mappers/respondent.mapper'; + +describe('RespondentMapper', () => { + const prismaRespondent = { + id: 1, + reviewId: 10, + respondentId: 20, + fullName: 'John Smith', + category: 'TEAM' as PrismaRespondent['category'], + responseStatus: 'PENDING' as PrismaRespondent['responseStatus'], + respondentNote: 'note', + hrNote: 'hr note', + positionId: 3, + positionTitle: 'Engineer', + teamId: 4, + teamTitle: 'Team A', + invitedAt: new Date('2025-01-01T00:00:00.000Z'), + canceledAt: null, + respondedAt: null, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaRespondent; + + describe('toDomain', () => { + it('converts a prisma row into a RespondentDomain instance', () => { + const domain = RespondentMapper.toDomain(prismaRespondent); + + expect(domain).toBeInstanceOf(RespondentDomain); + expect(domain.id).toBe(1); + expect(domain.reviewId).toBe(10); + expect(domain.respondentId).toBe(20); + expect(domain.fullName).toBe('John Smith'); + expect(domain.category).toBe(RespondentCategory.TEAM); + expect(domain.responseStatus).toBe(ResponseStatus.PENDING); + expect(domain.teamId).toBe(4); + expect(domain.teamTitle).toBe('Team A'); + }); + }); + + describe('toPrisma', () => { + it('serialises a RespondentDomain into a Prisma create input', () => { + const domain = RespondentDomain.create({ + reviewId: 10, + respondentId: 20, + fullName: 'John Smith', + category: RespondentCategory.SELF_ASSESSMENT, + responseStatus: ResponseStatus.IN_PROGRESS, + positionId: 3, + positionTitle: 'Engineer', + }); + + const prisma = RespondentMapper.toPrisma(domain); + + expect(prisma.reviewId).toBe(10); + expect(prisma.respondentId).toBe(20); + expect(prisma.fullName).toBe('John Smith'); + expect(prisma.category).toBe('SELF_ASSESSMENT'); + expect(prisma.responseStatus).toBe('IN_PROGRESS'); + expect(prisma.positionId).toBe(3); + expect(prisma.positionTitle).toBe('Engineer'); + }); + }); + + describe('enum conversions', () => { + it('uppercases the response status in both directions', () => { + expect( + RespondentMapper.toPrismaResponseStatus( + ResponseStatus.COMPLETED, + ), + ).toBe('COMPLETED'); + expect( + RespondentMapper.fromPrismaResponseStatus( + 'CANCELED' as PrismaRespondent['responseStatus'], + ), + ).toBe(ResponseStatus.CANCELED); + }); + + it('uppercases the category in both directions', () => { + expect( + RespondentMapper.toPrismaCategory(RespondentCategory.OTHER), + ).toBe('OTHER'); + expect( + RespondentMapper.fromPrismaCategory( + 'TEAM' as PrismaRespondent['category'], + ), + ).toBe(RespondentCategory.TEAM); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review-question-relation.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review-question-relation.mapper.spec.ts new file mode 100644 index 00000000..a8f17b94 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review-question-relation.mapper.spec.ts @@ -0,0 +1,80 @@ +import { ReviewQuestionRelation as PrismaReviewQuestionRelation } from '@intra/database'; +import { AnswerType } from '@intra/shared-kernel'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { ReviewQuestionRelationMapper } from 'src/contexts/feedback360/infrastructure/mappers/review-question-relation.mapper'; + +describe('ReviewQuestionRelationMapper', () => { + const prismaRelation = { + id: 1, + reviewId: 10, + questionId: 20, + questionTitle: 'How effective is teamwork?', + answerType: + 'NUMERICAL_SCALE' as PrismaReviewQuestionRelation['answerType'], + competenceId: 3, + competenceTitle: 'Teamwork', + competenceCode: 'TWK', + competenceDescription: 'Working with peers', + isForSelfassessment: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaReviewQuestionRelation; + + describe('toDomain', () => { + it('converts a prisma row into a ReviewQuestionRelationDomain', () => { + const domain = + ReviewQuestionRelationMapper.toDomain(prismaRelation); + + expect(domain).toBeInstanceOf(ReviewQuestionRelationDomain); + expect(domain.id).toBe(1); + expect(domain.reviewId).toBe(10); + expect(domain.questionId).toBe(20); + expect(domain.questionTitle).toBe('How effective is teamwork?'); + expect(domain.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(domain.competenceId).toBe(3); + expect(domain.competenceTitle).toBe('Teamwork'); + expect(domain.competenceCode).toBe('TWK'); + expect(domain.competenceDescription).toBe('Working with peers'); + expect(domain.isForSelfassessment).toBe(true); + }); + + it('normalises null competence code/description and isForSelfassessment', () => { + const domain = ReviewQuestionRelationMapper.toDomain({ + ...prismaRelation, + competenceCode: null, + competenceDescription: null, + isForSelfassessment: null, + } as unknown as PrismaReviewQuestionRelation); + expect(domain.competenceCode).toBeNull(); + expect(domain.competenceDescription).toBeNull(); + expect(domain.isForSelfassessment).toBe(false); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input', () => { + const domain = ReviewQuestionRelationDomain.create({ + reviewId: 10, + questionId: 20, + questionTitle: 'How effective is teamwork?', + answerType: AnswerType.TEXT_FIELD, + competenceId: 3, + competenceTitle: 'Teamwork', + competenceCode: null, + competenceDescription: null, + isForSelfassessment: false, + }); + + const prisma = ReviewQuestionRelationMapper.toPrisma(domain); + + expect(prisma.reviewId).toBe(10); + expect(prisma.questionId).toBe(20); + expect(prisma.questionTitle).toBe('How effective is teamwork?'); + expect(prisma.answerType).toBe('TEXT_FIELD'); + expect(prisma.competenceId).toBe(3); + expect(prisma.competenceTitle).toBe('Teamwork'); + expect(prisma.competenceCode).toBeNull(); + expect(prisma.competenceDescription).toBeNull(); + expect(prisma.isForSelfassessment).toBe(false); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review-stage-history.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review-stage-history.mapper.spec.ts new file mode 100644 index 00000000..ae29bc45 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review-stage-history.mapper.spec.ts @@ -0,0 +1,54 @@ +import { ReviewStageHistory as PrismaReviewStageHistory } from '@intra/database'; +import { ReviewStage } from '@intra/shared-kernel'; +import { ReviewStageHistoryDomain } from 'src/contexts/feedback360/domain/review-stage-history.domain'; +import { ReviewStageHistoryMapper } from 'src/contexts/feedback360/infrastructure/mappers/review-stage-history.mapper'; + +describe('ReviewStageHistoryMapper', () => { + const prismaHistory = { + id: 13, + reviewId: 50, + fromStage: 'NEW' as PrismaReviewStageHistory['fromStage'], + toStage: 'SELF_ASSESSMENT' as PrismaReviewStageHistory['toStage'], + changedById: 8, + changedByName: 'HR Manager', + reason: 'Review updated', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaReviewStageHistory; + + describe('toDomain', () => { + it('converts a prisma row into a ReviewStageHistoryDomain', () => { + const domain = ReviewStageHistoryMapper.toDomain(prismaHistory); + + expect(domain).toBeInstanceOf(ReviewStageHistoryDomain); + expect(domain.id).toBe(13); + expect(domain.reviewId).toBe(50); + expect(domain.fromStage).toBe(ReviewStage.NEW); + expect(domain.toStage).toBe(ReviewStage.SELF_ASSESSMENT); + expect(domain.changedById).toBe(8); + expect(domain.changedByName).toBe('HR Manager'); + expect(domain.reason).toBe('Review updated'); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input', () => { + const domain = ReviewStageHistoryDomain.create({ + reviewId: 50, + fromStage: ReviewStage.IN_PROGRESS, + toStage: ReviewStage.FINISHED, + reason: 'All responses collected from respondents', + }); + + const prisma = ReviewStageHistoryMapper.toPrisma(domain); + + expect(prisma.reviewId).toBe(50); + expect(prisma.fromStage).toBe('IN_PROGRESS'); + expect(prisma.toStage).toBe('FINISHED'); + expect(prisma.reason).toBe( + 'All responses collected from respondents', + ); + expect(prisma.changedById).toBeNull(); + expect(prisma.changedByName).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review.mapper.spec.ts new file mode 100644 index 00000000..9b945b72 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review.mapper.spec.ts @@ -0,0 +1,80 @@ +import { Review as PrismaReview } from '@intra/database'; +import { ReviewStage } from '@intra/shared-kernel'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewMapper } from 'src/contexts/feedback360/infrastructure/mappers/review.mapper'; + +describe('ReviewMapper', () => { + const prismaReview = { + id: 1, + rateeId: 10, + rateeFullName: 'Jane Doe', + rateePositionId: 3, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR Manager', + hrNote: 'note', + teamId: 4, + teamTitle: 'Team A', + managerId: 5, + managerFullName: 'Manager Name', + managerPositionId: 6, + managerPositionTitle: 'Engineering Lead', + cycleId: 50, + stage: 'IN_PROGRESS' as PrismaReview['stage'], + reportId: 99, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-01T01:00:00.000Z'), + } as PrismaReview; + + describe('toDomain', () => { + it('converts a prisma row into a ReviewDomain instance', () => { + const domain = ReviewMapper.toDomain(prismaReview); + + expect(domain).toBeInstanceOf(ReviewDomain); + expect(domain.id).toBe(1); + expect(domain.rateeFullName).toBe('Jane Doe'); + expect(domain.stage).toBe(ReviewStage.IN_PROGRESS); + expect(domain.cycleId).toBe(50); + expect(domain.reportId).toBe(99); + }); + }); + + describe('toPrisma', () => { + it('serialises a ReviewDomain into a Prisma create input', () => { + const domain = ReviewDomain.create({ + rateeId: 10, + rateeFullName: 'Jane Doe', + rateePositionId: 3, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR Manager', + stage: ReviewStage.SELF_ASSESSMENT, + }); + + const prisma = ReviewMapper.toPrisma(domain); + + expect(prisma.rateeId).toBe(10); + expect(prisma.rateeFullName).toBe('Jane Doe'); + expect(prisma.hrId).toBe(2); + expect(prisma.stage).toBe('SELF_ASSESSMENT'); + expect(prisma.cycleId).toBeNull(); + expect(prisma.reportId).toBeNull(); + }); + }); + + describe('stage conversions', () => { + it('uppercases the stage when going to Prisma', () => { + expect(ReviewMapper.toPrismaStage(ReviewStage.IN_PROGRESS)).toBe( + 'IN_PROGRESS', + ); + }); + + it('uppercases the stage when coming from Prisma', () => { + expect( + ReviewMapper.fromPrismaStage( + 'ANALYSIS' as PrismaReview['stage'], + ), + ).toBe(ReviewStage.ANALYSIS); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/reviewer.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/reviewer.mapper.spec.ts new file mode 100644 index 00000000..cebd8811 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/reviewer.mapper.spec.ts @@ -0,0 +1,62 @@ +import { Reviewer as PrismaReviewer } from '@intra/database'; +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; +import { ReviewerMapper } from 'src/contexts/feedback360/infrastructure/mappers/reviewer.mapper'; + +describe('ReviewerMapper', () => { + const prismaReviewer = { + id: 1, + reviewId: 10, + reviewerId: 20, + fullName: 'Reviewer One', + positionId: 3, + positionTitle: 'Senior Engineer', + teamId: 4, + teamTitle: 'Team B', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaReviewer; + + describe('toDomain', () => { + it('converts a prisma row into a ReviewerDomain instance', () => { + const domain = ReviewerMapper.toDomain(prismaReviewer); + + expect(domain).toBeInstanceOf(ReviewerDomain); + expect(domain.id).toBe(1); + expect(domain.reviewId).toBe(10); + expect(domain.reviewerId).toBe(20); + expect(domain.fullName).toBe('Reviewer One'); + expect(domain.positionId).toBe(3); + expect(domain.positionTitle).toBe('Senior Engineer'); + expect(domain.teamId).toBe(4); + expect(domain.teamTitle).toBe('Team B'); + expect(domain.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + }); + + describe('toPrisma', () => { + it('serialises a ReviewerDomain into a Prisma create input', () => { + const domain = ReviewerDomain.create({ + reviewId: 10, + reviewerId: 20, + fullName: 'Reviewer One', + positionId: 3, + positionTitle: 'Senior Engineer', + teamId: null, + teamTitle: null, + }); + + const prisma = ReviewerMapper.toPrisma(domain); + + expect(prisma).toEqual({ + reviewId: 10, + reviewerId: 20, + fullName: 'Reviewer One', + positionId: 3, + positionTitle: 'Senior Engineer', + teamId: null, + teamTitle: null, + }); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/controllers/cluster-score-analytics.controller.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/controllers/cluster-score-analytics.controller.spec.ts new file mode 100644 index 00000000..c4723de9 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/controllers/cluster-score-analytics.controller.spec.ts @@ -0,0 +1,125 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import Decimal from 'decimal.js'; +import { ClusterScoreAnalyticsService } from 'src/contexts/feedback360/application/services/cluster-score-analytics.service'; +import { ClusterScoreAnalyticsDomain } from 'src/contexts/feedback360/domain/cluster-score-analytics.domain'; +import { ClusterScoreAnalyticsController } from 'src/contexts/feedback360/presentation/http/controllers/cluster-score-analytics.controller'; + +function buildAnalytics(): ClusterScoreAnalyticsDomain { + return ClusterScoreAnalyticsDomain.create({ + id: 1, + cycleId: 100, + clusterId: 2, + lowerBound: new Decimal(0), + upperBound: new Decimal(5), + employeesCount: new Decimal(12), + employeeDensity: new Decimal('0.5'), + minScore: new Decimal(1), + maxScore: new Decimal(4), + averageScore: new Decimal(2), + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +describe('ClusterScoreAnalyticsController', () => { + let controller: ClusterScoreAnalyticsController; + let analytics: any; + + beforeEach(() => { + analytics = { + upsert: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + getByCycleId: jest.fn(), + }; + + controller = new ClusterScoreAnalyticsController( + analytics as unknown as ClusterScoreAnalyticsService, + ); + }); + + describe('upsert', () => { + it('forwards every dto field and maps the response', async () => { + analytics.upsert.mockResolvedValue(buildAnalytics()); + const dto = { + cycleId: 100, + clusterId: 2, + lowerBound: 0, + upperBound: 5, + employeesCount: 12, + employeeDensity: 0.5, + minScore: 1, + maxScore: 4, + averageScore: 2, + } as any; + + const result = await controller.upsert(dto); + + expect(analytics.upsert).toHaveBeenCalledWith({ + cycleId: 100, + clusterId: 2, + lowerBound: 0, + upperBound: 5, + employeesCount: 12, + employeeDensity: 0.5, + minScore: 1, + maxScore: 4, + averageScore: 2, + }); + expect(result.id).toBe(1); + expect(result.cycleId).toBe(100); + }); + }); + + describe('search', () => { + it('maps each analytics row to a response', async () => { + analytics.search.mockResolvedValue([buildAnalytics()]); + const result = await controller.search({} as any); + expect(analytics.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('parses the id and calls the service', async () => { + analytics.getById.mockResolvedValue(buildAnalytics()); + const result = await controller.getById('1'); + expect(analytics.getById).toHaveBeenCalledWith(1); + expect(result.id).toBe(1); + }); + }); + + describe('update', () => { + it('parses the id and forwards the patch', async () => { + analytics.update.mockResolvedValue(buildAnalytics()); + const dto = { employeesCount: 20 } as any; + + const result = await controller.update('1', dto); + + expect(analytics.update).toHaveBeenCalledWith(1, dto); + expect(result.id).toBe(1); + }); + }); + + describe('delete', () => { + it('parses the id and calls the service', async () => { + await controller.delete('1'); + expect(analytics.delete).toHaveBeenCalledWith(1); + }); + }); + + describe('getByCycleId', () => { + it('maps each analytics row to a response', async () => { + analytics.getByCycleId.mockResolvedValue([buildAnalytics()]); + const result = await controller.getByCycleId('100'); + expect(analytics.getByCycleId).toHaveBeenCalledWith(100); + expect(result).toHaveLength(1); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/controllers/cluster-scores.controller.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/controllers/cluster-scores.controller.spec.ts new file mode 100644 index 00000000..4f13d5af --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/controllers/cluster-scores.controller.spec.ts @@ -0,0 +1,140 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { ClusterScoreWithRelationsDomain } from 'src/contexts/feedback360/domain/cluster-score-with-relations.domain'; +import { ClusterScoreDomain } from 'src/contexts/feedback360/domain/cluster-score.domain'; +import { ClusterScoresController } from 'src/contexts/feedback360/presentation/http/controllers/cluster-scores.controller'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; + +function buildScore(): ClusterScoreDomain { + return ClusterScoreDomain.create({ + id: 1, + cycleId: 100, + clusterId: 2, + rateeId: 3, + reviewId: 4, + score: 3.5, + answersCount: 5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +function buildScoreWithRelations(): ClusterScoreWithRelationsDomain { + return ClusterScoreWithRelationsDomain.create({ + id: 1, + cycleId: 100, + clusterId: 2, + rateeId: 3, + reviewId: 4, + score: 3.5, + answersCount: 5, + cluster: ClusterDomain.create({ + id: 2, + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'Soft Skills', + description: 'desc', + }), + ratee: UserDomain.create({ + id: 3, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + roles: [IdentityRole.EMPLOYEE], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-01T00:00:00.000Z'), + }), + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +describe('ClusterScoresController', () => { + let controller: ClusterScoresController; + let reviews: any; + + beforeEach(() => { + reviews = { + upsertClusterScore: jest.fn(), + listClusterScores: jest.fn(), + getClusterScoreById: jest.fn(), + removeClusterScore: jest.fn(), + getClusterScoreByCycleId: jest.fn(), + }; + + controller = new ClusterScoresController( + reviews as unknown as ReviewService, + ); + }); + + describe('upsert', () => { + it('forwards the dto and returns a mapped response', async () => { + reviews.upsertClusterScore.mockResolvedValue(buildScore()); + const dto = { + cycleId: 100, + clusterId: 2, + rateeId: 3, + reviewId: 4, + score: 3.5, + } as any; + + const result = await controller.upsert(dto); + + expect(reviews.upsertClusterScore).toHaveBeenCalledWith(dto); + expect(result.id).toBe(1); + expect(result.score).toBeCloseTo(3.5, 4); + }); + }); + + describe('list', () => { + it('maps each score to a response', async () => { + reviews.listClusterScores.mockResolvedValue([buildScore()]); + const result = await controller.list({} as any); + expect(reviews.listClusterScores).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('returns the with-relations response', async () => { + reviews.getClusterScoreById.mockResolvedValue( + buildScoreWithRelations(), + ); + + const result = await controller.getById('1'); + + expect(reviews.getClusterScoreById).toHaveBeenCalledWith(1); + expect(result.cluster.title).toBe('Soft Skills'); + expect(result.ratee.fullName).toBe('Jane Doe'); + }); + }); + + describe('delete', () => { + it('parses the id and calls remove', async () => { + await controller.delete('5'); + expect(reviews.removeClusterScore).toHaveBeenCalledWith(5); + }); + }); + + describe('getByCycleId', () => { + it('maps every aggregate to a response', async () => { + reviews.getClusterScoreByCycleId.mockResolvedValue([ + buildScoreWithRelations(), + ]); + + const result = await controller.getByCycleId('100'); + + expect(reviews.getClusterScoreByCycleId).toHaveBeenCalledWith(100); + expect(result).toHaveLength(1); + expect(result[0].cluster.id).toBe(2); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/controllers/cycles.controller.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/controllers/cycles.controller.spec.ts new file mode 100644 index 00000000..8bc48262 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/controllers/cycles.controller.spec.ts @@ -0,0 +1,129 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { CycleStage } from '@intra/shared-kernel'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { CyclesController } from 'src/contexts/feedback360/presentation/http/controllers/cycles.controller'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; + +function buildCycle( + overrides: Partial[0]> = {}, +): CycleDomain { + return CycleDomain.create({ + id: 1, + title: 'Q1 2025', + hrId: 7, + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-03-31T00:00:00.000Z'), + ...overrides, + }); +} + +describe('CyclesController', () => { + let controller: CyclesController; + let cycles: any; + + beforeEach(() => { + cycles = { + create: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + forceFinish: jest.fn(), + }; + + controller = new CyclesController(cycles as unknown as CycleService); + }); + + describe('create', () => { + it('returns the response shape produced by the http mapper', async () => { + cycles.create.mockResolvedValue(buildCycle()); + const dto = { + title: 'Q1 2025', + hrId: 7, + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-03-31T00:00:00.000Z'), + } as any; + + const result = await controller.create(dto); + + expect(cycles.create).toHaveBeenCalledWith(dto); + expect(result.id).toBe(1); + expect(result.title).toBe('Q1 2025'); + }); + }); + + describe('search', () => { + it('maps every cycle to a response', async () => { + cycles.search.mockResolvedValue([ + buildCycle({ id: 1 }), + buildCycle({ id: 2 }), + ]); + + const result = await controller.search({} as any); + + expect(cycles.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(2); + expect(result[0].id).toBe(1); + expect(result[1].id).toBe(2); + }); + }); + + describe('getById', () => { + it('parses the id as a number and forwards to the service', async () => { + cycles.getById.mockResolvedValue(buildCycle({ id: 42 })); + + const result = await controller.getById('42'); + + expect(cycles.getById).toHaveBeenCalledWith(42); + expect(result.id).toBe(42); + }); + }); + + describe('update', () => { + it('parses the id and forwards the patch', async () => { + const updated = buildCycle({ stage: CycleStage.ACTIVE }); + cycles.update.mockResolvedValue(updated); + + const result = await controller.update('1', { + stage: CycleStage.ACTIVE, + } as any); + + expect(cycles.update).toHaveBeenCalledWith(1, { + stage: CycleStage.ACTIVE, + }); + expect(result.stage).toBe(CycleStage.ACTIVE); + }); + }); + + describe('delete', () => { + it('parses the id and calls service.delete', async () => { + await controller.delete('1'); + expect(cycles.delete).toHaveBeenCalledWith(1); + }); + }); + + describe('forceFinish', () => { + it('passes actor id and full name to the service and returns the refreshed cycle', async () => { + cycles.getById.mockResolvedValue( + buildCycle({ stage: CycleStage.FINISHED }), + ); + const actor = UserDomain.create({ + id: 9, + firstName: 'HR', + lastName: 'Admin', + email: 'hr@example.com', + }); + + const result = await controller.forceFinish('1', actor); + + expect(cycles.forceFinish).toHaveBeenCalledWith(1, 9, 'HR Admin'); + expect(cycles.getById).toHaveBeenCalledWith(1); + expect(result.stage).toBe(CycleStage.FINISHED); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/controllers/questions.controller.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/controllers/questions.controller.spec.ts new file mode 100644 index 00000000..7bc53a0d --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/controllers/questions.controller.spec.ts @@ -0,0 +1,66 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { AnswerType } from '@intra/shared-kernel'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { QuestionsController } from 'src/contexts/feedback360/presentation/http/controllers/questions.controller'; + +describe('QuestionsController', () => { + let controller: QuestionsController; + let reviews: any; + + beforeEach(() => { + reviews = { + createQuestion: jest.fn(), + listQuestions: jest.fn(), + deleteQuestion: jest.fn(), + }; + + controller = new QuestionsController( + reviews as unknown as ReviewService, + ); + }); + + describe('create', () => { + it('maps the result to a response', async () => { + reviews.createQuestion.mockResolvedValue( + QuestionDomain.create({ + id: 1, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + }), + ); + + const result = await controller.create({} as any); + expect(reviews.createQuestion).toHaveBeenCalledWith({}); + expect(result.id).toBe(1); + expect(result.title).toBe('Q'); + }); + }); + + describe('search', () => { + it('maps every question to a response', async () => { + reviews.listQuestions.mockResolvedValue([ + QuestionDomain.create({ + id: 1, + title: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + }), + ]); + + const result = await controller.search({} as any); + expect(reviews.listQuestions).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('delete', () => { + it('parses the id and forwards the call', async () => { + await controller.delete('7'); + expect(reviews.deleteQuestion).toHaveBeenCalledWith(7); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/controllers/reviews.controller.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/controllers/reviews.controller.spec.ts new file mode 100644 index 00000000..89cd7faa --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/controllers/reviews.controller.spec.ts @@ -0,0 +1,383 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { + AnswerType, + IdentityRole, + RespondentCategory, + ResponseStatus, + ReviewStage, +} from '@intra/shared-kernel'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; +import { ReviewController } from 'src/contexts/feedback360/presentation/http/controllers/reviews.controller'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 1, + rateeId: 11, + rateeFullName: 'Ratee', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + ...overrides, + }); +} + +describe('ReviewController', () => { + let controller: ReviewController; + let reviews: any; + const actor = UserDomain.create({ + id: 7, + firstName: 'HR', + lastName: 'Admin', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + + beforeEach(() => { + reviews = { + create: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + attachQuestion: jest.fn(), + listQuestionRelations: jest.fn(), + detachQuestion: jest.fn(), + addAnswer: jest.fn(), + listAnswers: jest.fn(), + countReviewAnswers: jest.fn(), + addRespondent: jest.fn(), + listRespondents: jest.fn(), + updateRespondent: jest.fn(), + removeRespondent: jest.fn(), + addReviewer: jest.fn(), + listReviewers: jest.fn(), + removeReviewer: jest.fn(), + forceFinishReview: jest.fn(), + }; + + controller = new ReviewController(reviews as unknown as ReviewService); + }); + + describe('create', () => { + it('returns the http response for the created review', async () => { + reviews.create.mockResolvedValue(buildReview()); + const dto = { rateeId: 11 } as any; + + const result = await controller.create(dto); + + expect(reviews.create).toHaveBeenCalledWith(dto); + expect(result.id).toBe(1); + expect(result.rateeFullName).toBe('Ratee'); + }); + }); + + describe('search', () => { + it('passes actor to the service and maps results', async () => { + reviews.search.mockResolvedValue([buildReview({ id: 1 })]); + + const result = await controller.search({} as any, actor); + + expect(reviews.search).toHaveBeenCalledWith({}, actor); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('parses the id and forwards the actor', async () => { + reviews.getById.mockResolvedValue(buildReview({ id: 42 })); + const result = await controller.getById('42', actor); + expect(reviews.getById).toHaveBeenCalledWith(42, actor); + expect(result.id).toBe(42); + }); + }); + + describe('update', () => { + it('passes actor and parsed id to the service', async () => { + reviews.update.mockResolvedValue( + buildReview({ stage: ReviewStage.SELF_ASSESSMENT }), + ); + const dto = { stage: ReviewStage.SELF_ASSESSMENT } as any; + + const result = await controller.update('1', dto, actor); + + expect(reviews.update).toHaveBeenCalledWith(1, dto, actor); + expect(result.stage).toBe(ReviewStage.SELF_ASSESSMENT); + }); + }); + + describe('delete', () => { + it('parses the id and calls the service', async () => { + await controller.delete('1'); + expect(reviews.delete).toHaveBeenCalledWith(1); + }); + }); + + describe('attachQuestion', () => { + it('forwards the dto with the parsed review id', async () => { + reviews.attachQuestion.mockResolvedValue( + ReviewQuestionRelationDomain.create({ + id: 1, + reviewId: 1, + questionId: 5, + questionTitle: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 3, + competenceTitle: 'Teamwork', + }), + ); + + const result = await controller.attachQuestion('1', { + questionTemplateId: 5, + } as any); + + expect(reviews.attachQuestion).toHaveBeenCalledWith({ + reviewId: 1, + questionTemplateId: 5, + }); + expect(result.questionId).toBe(5); + }); + }); + + describe('listQuestions', () => { + it('forwards the request to listQuestionRelations and maps results', async () => { + reviews.listQuestionRelations.mockResolvedValue([ + ReviewQuestionRelationDomain.create({ + id: 1, + reviewId: 1, + questionId: 5, + questionTitle: 'Q', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 3, + competenceTitle: 'C', + }), + ]); + + const result = await controller.listQuestions( + '1', + {} as any, + actor, + ); + + expect(reviews.listQuestionRelations).toHaveBeenCalledWith( + 1, + {}, + actor, + ); + expect(result).toHaveLength(1); + }); + }); + + describe('detachQuestion', () => { + it('parses both path params', async () => { + await controller.detachQuestion('1', '5'); + expect(reviews.detachQuestion).toHaveBeenCalledWith(1, 5); + }); + }); + + describe('addAnswer', () => { + it('forwards a numerical answer payload', async () => { + const created = AnswerDomain.create({ + id: 1, + reviewId: 1, + questionId: 5, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }); + reviews.addAnswer.mockResolvedValue(created); + + const dto = { + questionId: 5, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + } as any; + + const result = await controller.addAnswer('1', dto, actor); + + expect(reviews.addAnswer).toHaveBeenCalledWith( + { + reviewId: 1, + questionId: 5, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + textValue: undefined, + }, + actor, + ); + expect(result.numericalValue).toBe(4); + }); + }); + + describe('listAnswers', () => { + it('passes the respondentCategory query through', async () => { + reviews.listAnswers.mockResolvedValue([]); + await controller.listAnswers( + '1', + { respondentCategory: RespondentCategory.TEAM } as any, + actor, + ); + expect(reviews.listAnswers).toHaveBeenCalledWith( + 1, + RespondentCategory.TEAM, + actor, + ); + }); + }); + + describe('countAnswers', () => { + it('returns the count from the service', async () => { + reviews.countReviewAnswers.mockResolvedValue(5); + const count = await controller.countAnswers('1', actor); + expect(reviews.countReviewAnswers).toHaveBeenCalledWith(1, actor); + expect(count).toBe(5); + }); + }); + + describe('addRespondent', () => { + it('builds the payload from path id and body', async () => { + const created = RespondentDomain.create({ + id: 99, + reviewId: 1, + respondentId: 50, + fullName: 'John Doe', + category: RespondentCategory.TEAM, + responseStatus: ResponseStatus.PENDING, + positionId: 3, + positionTitle: 'Engineer', + }); + reviews.addRespondent.mockResolvedValue(created); + + const dto = { + respondentId: 50, + fullName: 'John Doe', + category: RespondentCategory.TEAM, + responseStatus: ResponseStatus.PENDING, + positionId: 3, + positionTitle: 'Engineer', + } as any; + + const result = await controller.addRespondent('1', dto); + + const call = reviews.addRespondent.mock.calls[0][0]; + expect(call.reviewId).toBe(1); + expect(call.respondentId).toBe(50); + expect(result.id).toBe(99); + }); + }); + + describe('listRespondents', () => { + it('forwards the call with actor and query', async () => { + reviews.listRespondents.mockResolvedValue([]); + await controller.listRespondents('1', {} as any, actor); + expect(reviews.listRespondents).toHaveBeenCalledWith(1, {}, actor); + }); + }); + + describe('updateRespondent', () => { + it('parses the relation id and forwards the dto and actor', async () => { + reviews.updateRespondent.mockResolvedValue( + RespondentDomain.create({ + id: 7, + reviewId: 1, + respondentId: 50, + fullName: 'John', + category: RespondentCategory.TEAM, + positionId: 3, + positionTitle: 'Engineer', + }), + ); + + await controller.updateRespondent( + '7', + { hrNote: 'x' } as any, + actor, + ); + expect(reviews.updateRespondent).toHaveBeenCalledWith( + 7, + { hrNote: 'x' }, + actor, + ); + }); + }); + + describe('deleteRespondent', () => { + it('parses the id and forwards the call', async () => { + await controller.deleteRespondent('5'); + expect(reviews.removeRespondent).toHaveBeenCalledWith(5); + }); + }); + + describe('addReviewer', () => { + it('builds the payload from the path id and body', async () => { + reviews.addReviewer.mockResolvedValue( + ReviewerDomain.create({ + id: 1, + reviewId: 1, + reviewerId: 80, + fullName: 'Reviewer', + positionId: 3, + positionTitle: 'Eng', + }), + ); + + await controller.addReviewer('1', { + reviewerId: 80, + fullName: 'Reviewer', + positionId: 3, + positionTitle: 'Eng', + } as any); + + const call = reviews.addReviewer.mock.calls[0][0]; + expect(call.reviewId).toBe(1); + expect(call.reviewerId).toBe(80); + }); + }); + + describe('listReviewers', () => { + it('forwards the call and maps results', async () => { + reviews.listReviewers.mockResolvedValue([]); + await controller.listReviewers('1', {} as any, actor); + expect(reviews.listReviewers).toHaveBeenCalledWith(1, {}, actor); + }); + }); + + describe('deleteReviewer', () => { + it('parses the id and forwards the call', async () => { + await controller.deleteReviewer('5'); + expect(reviews.removeReviewer).toHaveBeenCalledWith(5); + }); + }); + + describe('forceFinish', () => { + it('forwards the actor info and returns the refreshed review', async () => { + reviews.getById.mockResolvedValue( + buildReview({ stage: ReviewStage.FINISHED }), + ); + + const result = await controller.forceFinish('1', actor); + + expect(reviews.forceFinishReview).toHaveBeenCalledWith( + 1, + actor.id, + actor.fullName, + ); + expect(reviews.getById).toHaveBeenCalledWith(1); + expect(result.stage).toBe(ReviewStage.FINISHED); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/answer.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/answer.http.mapper.spec.ts new file mode 100644 index 00000000..ee04dbf1 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/answer.http.mapper.spec.ts @@ -0,0 +1,46 @@ +import { AnswerType, RespondentCategory } from '@intra/shared-kernel'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { AnswerHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/answer.http.mapper'; + +describe('AnswerHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = AnswerDomain.create({ + id: 1, + reviewId: 10, + questionId: 20, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = AnswerHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.reviewId).toBe(10); + expect(response.questionId).toBe(20); + expect(response.respondentCategory).toBe(RespondentCategory.TEAM); + expect(response.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(response.numericalValue).toBe(4); + expect(response.textValue).toBeNull(); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('returns null for missing numerical and text values', () => { + const domain = AnswerDomain.create({ + reviewId: 10, + questionId: 20, + respondentCategory: RespondentCategory.OTHER, + answerType: AnswerType.TEXT_FIELD, + }); + + const response = AnswerHttpMapper.toResponse(domain); + + expect(response.numericalValue).toBeNull(); + expect(response.textValue).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/cluster-score-analytics.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/cluster-score-analytics.http.mapper.spec.ts new file mode 100644 index 00000000..60a24604 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/cluster-score-analytics.http.mapper.spec.ts @@ -0,0 +1,43 @@ +import Decimal from 'decimal.js'; +import { ClusterScoreAnalyticsDomain } from 'src/contexts/feedback360/domain/cluster-score-analytics.domain'; +import { ClusterScoreAnalyticsHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/cluster-score-analytics.http.mapper'; + +describe('ClusterScoreAnalyticsHttpMapper', () => { + describe('toResponse', () => { + it('maps the domain into a response, rounding numeric fields to 4dp', () => { + const domain = ClusterScoreAnalyticsDomain.create({ + id: 1, + cycleId: 100, + clusterId: 2, + lowerBound: new Decimal(0), + upperBound: new Decimal(5), + employeesCount: new Decimal(12), + employeeDensity: new Decimal('0.123456'), + minScore: new Decimal('1.234567'), + maxScore: new Decimal('4.876543'), + averageScore: new Decimal('3.4'), + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = ClusterScoreAnalyticsHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.cycleId).toBe(100); + expect(response.clusterId).toBe(2); + expect(response.lowerBound).toBe(0); + expect(response.upperBound).toBe(5); + expect(response.employeesCount).toBe(12); + expect(response.employeeDensity).toBeCloseTo(0.1235, 4); + expect(response.minScore).toBeCloseTo(1.2346, 4); + expect(response.maxScore).toBeCloseTo(4.8765, 4); + expect(response.averageScore).toBeCloseTo(3.4, 4); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/cluster-score.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/cluster-score.http.mapper.spec.ts new file mode 100644 index 00000000..bc7a2cbe --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/cluster-score.http.mapper.spec.ts @@ -0,0 +1,119 @@ +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { ClusterScoreWithRelationsDomain } from 'src/contexts/feedback360/domain/cluster-score-with-relations.domain'; +import { ClusterScoreDomain } from 'src/contexts/feedback360/domain/cluster-score.domain'; +import { ClusterScoreHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/cluster-score.http.mapper'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; + +describe('ClusterScoreHttpMapper', () => { + describe('toResponse', () => { + it('maps the domain fields and rounds score to 4dp', () => { + const domain = ClusterScoreDomain.create({ + id: 1, + cycleId: 100, + clusterId: 2, + rateeId: 3, + reviewId: 4, + score: 3.456789, + answersCount: 5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = ClusterScoreHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.cycleId).toBe(100); + expect(response.clusterId).toBe(2); + expect(response.rateeId).toBe(3); + expect(response.reviewId).toBe(4); + expect(response.score).toBeCloseTo(3.4568, 4); + expect(response.answersCount).toBe(5); + }); + + it('emits null cycleId when missing', () => { + const domain = ClusterScoreDomain.create({ + id: 1, + clusterId: 2, + rateeId: 3, + reviewId: 4, + score: 1, + answersCount: 1, + }); + + const response = ClusterScoreHttpMapper.toResponse(domain); + expect(response.cycleId).toBeNull(); + }); + }); + + describe('toResponseWithRelations', () => { + it('includes the cluster and ratee in the response', () => { + const cluster = ClusterDomain.create({ + id: 2, + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'Soft Skills', + description: 'Communication', + }); + + const ratee = UserDomain.create({ + id: 11, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + roles: [IdentityRole.EMPLOYEE], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const aggregate = ClusterScoreWithRelationsDomain.create({ + id: 1, + cycleId: 100, + clusterId: cluster.id!, + rateeId: ratee.id!, + reviewId: 4, + score: 3.5, + answersCount: 5, + cluster, + ratee, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = + ClusterScoreHttpMapper.toResponseWithRelations(aggregate); + + expect(response.cluster.id).toBe(2); + expect(response.cluster.title).toBe('Soft Skills'); + expect(response.ratee.id).toBe(11); + expect(response.ratee.fullName).toBe('Jane Doe'); + expect(response.score).toBeCloseTo(3.5, 4); + }); + }); + + describe('toScoreRoundedNumber', () => { + it('rounds a Decimal to a 4-decimal-place number', () => { + expect( + ClusterScoreHttpMapper.toScoreRoundedNumber( + new Decimal('3.456789'), + ), + ).toBeCloseTo(3.4568, 4); + }); + + it('rounds a plain number to a 4-decimal-place number', () => { + expect(ClusterScoreHttpMapper.toScoreRoundedNumber(2.5)).toBe(2.5); + }); + + it('returns null when the input is null or undefined', () => { + expect( + ClusterScoreHttpMapper.toScoreRoundedNumber(null), + ).toBeNull(); + expect( + ClusterScoreHttpMapper.toScoreRoundedNumber(undefined), + ).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/cycle.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/cycle.http.mapper.spec.ts new file mode 100644 index 00000000..f28039fc --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/cycle.http.mapper.spec.ts @@ -0,0 +1,76 @@ +import { CycleStage } from '@intra/shared-kernel'; +import { CycleDomain } from 'src/contexts/feedback360/domain/cycle.domain'; +import { CycleHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/cycle.http.mapper'; +import { CycleResponse } from 'src/contexts/feedback360/presentation/http/models/cycle.response'; + +describe('CycleHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = CycleDomain.create({ + id: 1, + title: 'Q1 2025', + description: 'First cycle', + hrId: 7, + minRespondentsThreshold: 5, + stage: CycleStage.ACTIVE, + isActive: true, + startDate: new Date('2025-01-01T00:00:00.000Z'), + reviewDeadline: new Date('2025-02-01T00:00:00.000Z'), + approvalDeadline: new Date('2025-02-15T00:00:00.000Z'), + responseDeadline: new Date('2025-03-01T00:00:00.000Z'), + endDate: new Date('2025-03-31T00:00:00.000Z'), + createdAt: new Date('2024-12-31T00:00:00.000Z'), + updatedAt: new Date('2024-12-31T01:00:00.000Z'), + }); + + const response = CycleHttpMapper.toResponse(domain); + + expect(response).toBeInstanceOf(CycleResponse); + expect(response.id).toBe(1); + expect(response.title).toBe('Q1 2025'); + expect(response.description).toBe('First cycle'); + expect(response.hrId).toBe(7); + expect(response.minRespondentsThreshold).toBe(5); + expect(response.stage).toBe(CycleStage.ACTIVE); + expect(response.isActive).toBe(true); + expect(response.startDate).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.reviewDeadline).toEqual( + new Date('2025-02-01T00:00:00.000Z'), + ); + expect(response.approvalDeadline).toEqual( + new Date('2025-02-15T00:00:00.000Z'), + ); + expect(response.responseDeadline).toEqual( + new Date('2025-03-01T00:00:00.000Z'), + ); + expect(response.endDate).toEqual( + new Date('2025-03-31T00:00:00.000Z'), + ); + expect(response.createdAt).toEqual( + new Date('2024-12-31T00:00:00.000Z'), + ); + expect(response.updatedAt).toEqual( + new Date('2024-12-31T01:00:00.000Z'), + ); + }); + + it('emits explicit null values for missing optional fields', () => { + const domain = CycleDomain.create({ + id: 2, + title: 'Minimal', + hrId: 1, + startDate: new Date('2025-01-01T00:00:00.000Z'), + endDate: new Date('2025-12-31T00:00:00.000Z'), + }); + + const response = CycleHttpMapper.toResponse(domain); + + expect(response.description).toBeNull(); + expect(response.reviewDeadline).toBeNull(); + expect(response.approvalDeadline).toBeNull(); + expect(response.responseDeadline).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/question.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/question.http.mapper.spec.ts new file mode 100644 index 00000000..1fe3a6a1 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/question.http.mapper.spec.ts @@ -0,0 +1,47 @@ +import { AnswerType } from '@intra/shared-kernel'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { QuestionHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/question.http.mapper'; + +describe('QuestionHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = QuestionDomain.create({ + id: 1, + cycleId: 100, + questionTemplateId: 50, + title: 'How is teamwork?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 7, + isForSelfassessment: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = QuestionHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.cycleId).toBe(100); + expect(response.questionTemplateId).toBe(50); + expect(response.title).toBe('How is teamwork?'); + expect(response.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(response.competenceId).toBe(7); + expect(response.isForSelfassessment).toBe(true); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('returns null for missing foreign keys', () => { + const domain = QuestionDomain.create({ + id: 2, + title: 'q', + answerType: AnswerType.TEXT_FIELD, + }); + + const response = QuestionHttpMapper.toResponse(domain); + + expect(response.cycleId).toBeNull(); + expect(response.questionTemplateId).toBeNull(); + expect(response.competenceId).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/respondent.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/respondent.http.mapper.spec.ts new file mode 100644 index 00000000..f73a4853 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/respondent.http.mapper.spec.ts @@ -0,0 +1,64 @@ +import { RespondentCategory, ResponseStatus } from '@intra/shared-kernel'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { RespondentHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/respondent.http.mapper'; + +describe('RespondentHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = RespondentDomain.create({ + id: 1, + reviewId: 10, + respondentId: 20, + fullName: 'John Smith', + category: RespondentCategory.TEAM, + responseStatus: ResponseStatus.IN_PROGRESS, + respondentNote: 'note', + hrNote: 'hr note', + positionId: 3, + positionTitle: 'Engineer', + teamId: 4, + teamTitle: 'Team A', + invitedAt: new Date('2025-01-01T00:00:00.000Z'), + canceledAt: new Date('2025-01-02T00:00:00.000Z'), + respondedAt: new Date('2025-01-03T00:00:00.000Z'), + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-04T00:00:00.000Z'), + }); + + const response = RespondentHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.fullName).toBe('John Smith'); + expect(response.category).toBe(RespondentCategory.TEAM); + expect(response.responseStatus).toBe(ResponseStatus.IN_PROGRESS); + expect(response.respondentNote).toBe('note'); + expect(response.hrNote).toBe('hr note'); + expect(response.teamId).toBe(4); + expect(response.teamTitle).toBe('Team A'); + expect(response.invitedAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('returns null for missing optional fields', () => { + const domain = RespondentDomain.create({ + reviewId: 1, + respondentId: 2, + fullName: 'X', + category: RespondentCategory.OTHER, + positionId: 1, + positionTitle: 'Eng', + }); + + const response = RespondentHttpMapper.toResponse(domain); + + expect(response.respondentNote).toBeNull(); + expect(response.hrNote).toBeNull(); + expect(response.teamId).toBeNull(); + expect(response.teamTitle).toBeNull(); + expect(response.invitedAt).toBeNull(); + expect(response.canceledAt).toBeNull(); + expect(response.respondedAt).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/review-question-relation.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/review-question-relation.http.mapper.spec.ts new file mode 100644 index 00000000..4a6e0cfa --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/review-question-relation.http.mapper.spec.ts @@ -0,0 +1,53 @@ +import { AnswerType } from '@intra/shared-kernel'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { ReviewQuestionRelationHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/review-question-relation.http.mapper'; + +describe('ReviewQuestionRelationHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = ReviewQuestionRelationDomain.create({ + id: 1, + reviewId: 10, + questionId: 20, + questionTitle: 'How effective is teamwork?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 3, + competenceTitle: 'Teamwork', + competenceCode: 'TWK', + competenceDescription: 'Working with peers', + isForSelfassessment: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = + ReviewQuestionRelationHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.reviewId).toBe(10); + expect(response.questionId).toBe(20); + expect(response.questionTitle).toBe('How effective is teamwork?'); + expect(response.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(response.competenceId).toBe(3); + expect(response.competenceTitle).toBe('Teamwork'); + expect(response.competenceCode).toBe('TWK'); + expect(response.competenceDescription).toBe('Working with peers'); + expect(response.isForSelfassessment).toBe(true); + }); + + it('returns null for missing competence code/description', () => { + const domain = ReviewQuestionRelationDomain.create({ + reviewId: 10, + questionId: 20, + questionTitle: 'Q', + answerType: AnswerType.TEXT_FIELD, + competenceId: 3, + competenceTitle: 'Teamwork', + }); + + const response = + ReviewQuestionRelationHttpMapper.toResponse(domain); + expect(response.competenceCode).toBeNull(); + expect(response.competenceDescription).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/review.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/review.http.mapper.spec.ts new file mode 100644 index 00000000..efdc3c87 --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/review.http.mapper.spec.ts @@ -0,0 +1,71 @@ +import { ReviewStage } from '@intra/shared-kernel'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/review.http.mapper'; +import { ReviewResponse } from 'src/contexts/feedback360/presentation/http/models/review.response'; + +describe('ReviewHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = ReviewDomain.create({ + id: 100, + rateeId: 11, + rateeFullName: 'Jane Doe', + rateePositionId: 3, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR Manager', + hrNote: 'note', + teamId: 4, + teamTitle: 'Team A', + managerId: 5, + managerFullName: 'Manager Name', + managerPositionId: 6, + managerPositionTitle: 'Engineering Lead', + cycleId: 50, + stage: ReviewStage.IN_PROGRESS, + reportId: 99, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-01T01:00:00.000Z'), + }); + + const response = ReviewHttpMapper.toResponse(domain); + + expect(response).toBeInstanceOf(ReviewResponse); + expect(response.id).toBe(100); + expect(response.rateeId).toBe(11); + expect(response.rateeFullName).toBe('Jane Doe'); + expect(response.hrId).toBe(2); + expect(response.hrFullName).toBe('HR Manager'); + expect(response.hrNote).toBe('note'); + expect(response.teamId).toBe(4); + expect(response.managerId).toBe(5); + expect(response.cycleId).toBe(50); + expect(response.stage).toBe(ReviewStage.IN_PROGRESS); + expect(response.reportId).toBe(99); + }); + + it('emits explicit null for missing optional relations', () => { + const domain = ReviewDomain.create({ + id: 1, + rateeId: 1, + rateeFullName: 'Jane Doe', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR Manager', + }); + + const response = ReviewHttpMapper.toResponse(domain); + + expect(response.hrNote).toBeNull(); + expect(response.teamId).toBeNull(); + expect(response.teamTitle).toBeNull(); + expect(response.managerId).toBeNull(); + expect(response.managerFullName).toBeNull(); + expect(response.managerPositionId).toBeNull(); + expect(response.managerPositionTitle).toBeNull(); + expect(response.cycleId).toBeNull(); + expect(response.reportId).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/feedback360/presentation/mappers/reviewer.http.mapper.spec.ts b/apps/api/test/unit/contexts/feedback360/presentation/mappers/reviewer.http.mapper.spec.ts new file mode 100644 index 00000000..a68687bb --- /dev/null +++ b/apps/api/test/unit/contexts/feedback360/presentation/mappers/reviewer.http.mapper.spec.ts @@ -0,0 +1,45 @@ +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; +import { ReviewerHttpMapper } from 'src/contexts/feedback360/presentation/http/mappers/reviewer.http.mapper'; + +describe('ReviewerHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = ReviewerDomain.create({ + id: 1, + reviewId: 10, + reviewerId: 20, + fullName: 'Reviewer One', + positionId: 3, + positionTitle: 'Senior Engineer', + teamId: 4, + teamTitle: 'Team B', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = ReviewerHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.reviewId).toBe(10); + expect(response.reviewerId).toBe(20); + expect(response.fullName).toBe('Reviewer One'); + expect(response.positionId).toBe(3); + expect(response.positionTitle).toBe('Senior Engineer'); + expect(response.teamId).toBe(4); + expect(response.teamTitle).toBe('Team B'); + }); + + it('returns null for missing team fields', () => { + const domain = ReviewerDomain.create({ + reviewId: 1, + reviewerId: 2, + fullName: 'Reviewer', + positionId: 1, + positionTitle: 'Eng', + }); + + const response = ReviewerHttpMapper.toResponse(domain); + expect(response.teamId).toBeNull(); + expect(response.teamTitle).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/application/events/user-created.event.spec.ts b/apps/api/test/unit/contexts/identity/application/events/user-created.event.spec.ts new file mode 100644 index 00000000..1af330db --- /dev/null +++ b/apps/api/test/unit/contexts/identity/application/events/user-created.event.spec.ts @@ -0,0 +1,10 @@ +import { UserCreatedEvent } from 'src/contexts/identity/application/events/user-created.event'; + +describe('UserCreatedEvent', () => { + it('exposes userId and email as readonly fields', () => { + const event = new UserCreatedEvent(42, 'jane@example.com'); + + expect(event.userId).toBe(42); + expect(event.email).toBe('jane@example.com'); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/application/services/identity-role.service.spec.ts b/apps/api/test/unit/contexts/identity/application/services/identity-role.service.spec.ts new file mode 100644 index 00000000..04c3b40a --- /dev/null +++ b/apps/api/test/unit/contexts/identity/application/services/identity-role.service.spec.ts @@ -0,0 +1,59 @@ +import { IdentityRole } from '@intra/shared-kernel'; +import { Test } from '@nestjs/testing'; +import { IDENTITY_ROLE_REPOSITORY } from 'src/contexts/identity/application/ports/role.repository.port'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { RoleDomain } from 'src/contexts/identity/domain/role.domain'; + +function buildRole(code: IdentityRole): RoleDomain { + return new RoleDomain({ + id: 1, + code, + title: code, + description: null, + }); +} + +describe('IdentityRoleService', () => { + let service: IdentityRoleService; + let roles: any; + + beforeEach(async () => { + roles = { getAll: jest.fn(), findByCodes: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + IdentityRoleService, + { provide: IDENTITY_ROLE_REPOSITORY, useValue: roles }, + ], + }).compile(); + + service = module.get(IdentityRoleService); + }); + + describe('list', () => { + it('delegates to the repository getAll', async () => { + const items = [ + buildRole(IdentityRole.ADMIN), + buildRole(IdentityRole.HR), + ]; + roles.getAll.mockResolvedValue(items); + + await expect(service.list()).resolves.toBe(items); + expect(roles.getAll).toHaveBeenCalledTimes(1); + }); + }); + + describe('getByCodes', () => { + it('delegates to the repository findByCodes', async () => { + const items = [buildRole(IdentityRole.MANAGER)]; + roles.findByCodes.mockResolvedValue(items); + + const result = await service.getByCodes([IdentityRole.MANAGER]); + + expect(roles.findByCodes).toHaveBeenCalledWith([ + IdentityRole.MANAGER, + ]); + expect(result).toBe(items); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/application/services/identity-user.service.spec.ts b/apps/api/test/unit/contexts/identity/application/services/identity-user.service.spec.ts new file mode 100644 index 00000000..c9a5a254 --- /dev/null +++ b/apps/api/test/unit/contexts/identity/application/services/identity-user.service.spec.ts @@ -0,0 +1,342 @@ +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Test } from '@nestjs/testing'; +import { UserCreatedEvent } from 'src/contexts/identity/application/events/user-created.event'; +import { IDENTITY_ROLE_REPOSITORY } from 'src/contexts/identity/application/ports/role.repository.port'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { RoleDomain } from 'src/contexts/identity/domain/role.domain'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; + +function buildUser( + overrides: Partial[0]> = {}, +): UserDomain { + return UserDomain.create({ + id: 1, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + ...overrides, + }); +} + +describe('IdentityUserService', () => { + let service: IdentityUserService; + let users: any; + let roles: any; + let eventEmitter: { emit: jest.Mock }; + + beforeEach(async () => { + users = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + findByEmail: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + replaceRoles: jest.fn(), + }; + roles = { findByCodes: jest.fn() }; + eventEmitter = { emit: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + IdentityUserService, + { provide: IDENTITY_USER_REPOSITORY, useValue: users }, + { provide: IDENTITY_ROLE_REPOSITORY, useValue: roles }, + { provide: EventEmitter2, useValue: eventEmitter }, + ], + }).compile(); + + service = module.get(IdentityUserService); + }); + + describe('create', () => { + it('throws BadRequestException when a user with the email already exists', async () => { + users.findByEmail.mockResolvedValue(buildUser()); + + await expect( + service.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + } as any), + ).rejects.toBeInstanceOf(BadRequestException); + + expect(users.create).not.toHaveBeenCalled(); + expect(eventEmitter.emit).not.toHaveBeenCalled(); + }); + + it('builds a domain with default status, ids, and roles when not supplied', async () => { + users.findByEmail.mockResolvedValue(null); + users.create.mockImplementation(async (u: UserDomain) => + UserDomain.create({ ...u, id: 99 }), + ); + + await service.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + } as any); + + const passed = users.create.mock.calls[0][0] as UserDomain; + expect(passed).toBeInstanceOf(UserDomain); + expect(passed.status).toBe(IdentityStatus.ACTIVE); + expect(passed.positionId).toBeNull(); + expect(passed.teamId).toBeNull(); + expect(passed.managerId).toBeNull(); + expect(passed.roles).toEqual([]); + // The default fullName is "{lastName} {firstName}" via the + // service's buildFullName, not the domain's "first second last". + expect(passed.fullName).toBe('Doe Jane'); + }); + + it('honours the supplied fullName when present', async () => { + users.findByEmail.mockResolvedValue(null); + users.create.mockImplementation(async (u: UserDomain) => + UserDomain.create({ ...u, id: 99 }), + ); + + await service.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + fullName: 'Custom Full Name', + } as any); + + const passed = users.create.mock.calls[0][0] as UserDomain; + expect(passed.fullName).toBe('Custom Full Name'); + }); + + it('emits user.created with the persisted id and email', async () => { + users.findByEmail.mockResolvedValue(null); + users.create.mockResolvedValue(buildUser({ id: 99 })); + + await service.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + } as any); + + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'user.created', + expect.any(UserCreatedEvent), + ); + const event = eventEmitter.emit.mock + .calls[0][1] as UserCreatedEvent; + expect(event.userId).toBe(99); + expect(event.email).toBe('jane@example.com'); + }); + + it('does not emit user.created when the persisted user has no id', async () => { + users.findByEmail.mockResolvedValue(null); + users.create.mockResolvedValue( + UserDomain.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + }), + ); + + await service.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + } as any); + + expect(eventEmitter.emit).not.toHaveBeenCalled(); + }); + }); + + describe('search', () => { + it('delegates to the repository', async () => { + users.search.mockResolvedValue([buildUser()]); + + const result = await service.search({} as any); + expect(users.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('returns the user when found', async () => { + const user = buildUser(); + users.findById.mockResolvedValue(user); + + await expect(service.getById(1)).resolves.toBe(user); + expect(users.findById).toHaveBeenCalledWith(1, undefined); + }); + + it('forwards the withRoles option', async () => { + users.findById.mockResolvedValue(buildUser()); + + await service.getById(1, { withRoles: true }); + + expect(users.findById).toHaveBeenCalledWith(1, { withRoles: true }); + }); + + it('throws NotFoundException when missing', async () => { + users.findById.mockResolvedValue(null); + + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('findByEmail', () => { + it('delegates to the repository with the supplied opts', async () => { + users.findByEmail.mockResolvedValue(null); + + const result = await service.findByEmail('jane@example.com', { + withRoles: true, + }); + + expect(users.findByEmail).toHaveBeenCalledWith('jane@example.com', { + withRoles: true, + }); + expect(result).toBeNull(); + }); + }); + + describe('update', () => { + it('throws NotFoundException when the user does not exist', async () => { + users.findById.mockResolvedValue(null); + + await expect( + service.update(1, { firstName: 'X' } as any), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('rebuilds fullName when any name part is patched', async () => { + users.findById.mockResolvedValue( + buildUser({ firstName: 'Jane', lastName: 'Doe' }), + ); + users.updateById.mockResolvedValue(buildUser()); + + await service.update(1, { firstName: 'Jenny' } as any); + + const passed = users.updateById.mock.calls[0][1]; + expect(passed.firstName).toBe('Jenny'); + expect(passed.fullName).toBe('Doe Jenny'); + }); + + it('does not include fullName when no name part is patched', async () => { + users.findById.mockResolvedValue(buildUser()); + users.updateById.mockResolvedValue(buildUser()); + + await service.update(1, { positionId: 7 } as any); + + const passed = users.updateById.mock.calls[0][1]; + expect(passed.positionId).toBe(7); + expect(passed.fullName).toBeUndefined(); + }); + }); + + describe('delete', () => { + it('throws NotFoundException when the user is missing', async () => { + users.findById.mockResolvedValue(null); + + await expect(service.delete(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + expect(users.deleteById).not.toHaveBeenCalled(); + }); + + it('deletes when the user exists', async () => { + users.findById.mockResolvedValue(buildUser()); + + await service.delete(1); + + expect(users.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('replaceRoles', () => { + beforeEach(() => { + users.findById.mockResolvedValue(buildUser()); + }); + + it('throws BadRequestException when an unknown role is requested', async () => { + roles.findByCodes.mockResolvedValue([ + new RoleDomain({ code: IdentityRole.HR, title: 'HR' }), + ]); + + await expect( + service.replaceRoles(1, [IdentityRole.HR, IdentityRole.ADMIN]), + ).rejects.toBeInstanceOf(BadRequestException); + + expect(users.replaceRoles).not.toHaveBeenCalled(); + }); + + it('deduplicates roles and replaces them via the repository', async () => { + roles.findByCodes.mockResolvedValue([ + new RoleDomain({ code: IdentityRole.HR, title: 'HR' }), + new RoleDomain({ + code: IdentityRole.MANAGER, + title: 'Manager', + }), + ]); + users.replaceRoles.mockResolvedValue(buildUser()); + + await service.replaceRoles(1, [ + IdentityRole.HR, + IdentityRole.HR, + IdentityRole.MANAGER, + ]); + + expect(roles.findByCodes).toHaveBeenCalledWith([ + IdentityRole.HR, + IdentityRole.MANAGER, + ]); + expect(users.replaceRoles).toHaveBeenCalledWith(1, [ + IdentityRole.HR, + IdentityRole.MANAGER, + ]); + }); + }); + + describe('upsertExternalUser', () => { + it('returns the existing user when one with the email already exists', async () => { + const existing = buildUser({ id: 42 }); + users.findByEmail.mockResolvedValue(existing); + + const result = await service.upsertExternalUser({ + email: 'jane@example.com', + firstName: 'Jane', + lastName: 'Doe', + secondName: undefined, + }); + + expect(result).toBe(existing); + expect(users.create).not.toHaveBeenCalled(); + expect(eventEmitter.emit).not.toHaveBeenCalled(); + }); + + it('creates a new EMPLOYEE-role user and emits user.created when missing', async () => { + users.findByEmail.mockResolvedValue(null); + users.create.mockImplementation(async (u: UserDomain) => + UserDomain.create({ ...u, id: 99 }), + ); + + const result = await service.upsertExternalUser({ + email: 'jane@example.com', + firstName: 'Jane', + lastName: 'Doe', + secondName: 'M.', + avatarUrl: 'https://example.com/a.png', + }); + + expect(result.id).toBe(99); + expect(users.create).toHaveBeenCalledTimes(1); + const passed = users.create.mock.calls[0][0] as UserDomain; + expect(passed.roles).toEqual([IdentityRole.EMPLOYEE]); + expect(passed.fullName).toBe('Doe Jane M.'); + expect(eventEmitter.emit).toHaveBeenCalledWith( + 'user.created', + expect.any(UserCreatedEvent), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/domain/role.domain.spec.ts b/apps/api/test/unit/contexts/identity/domain/role.domain.spec.ts new file mode 100644 index 00000000..7dc660e6 --- /dev/null +++ b/apps/api/test/unit/contexts/identity/domain/role.domain.spec.ts @@ -0,0 +1,47 @@ +import { IdentityRole } from '@intra/shared-kernel'; +import { RoleDomain } from 'src/contexts/identity/domain/role.domain'; + +describe('RoleDomain', () => { + describe('constructor', () => { + it('creates a role with every supplied property', () => { + const role = new RoleDomain({ + id: 1, + code: IdentityRole.HR, + title: 'HR', + description: 'Human resources', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(role.id).toBe(1); + expect(role.code).toBe(IdentityRole.HR); + expect(role.title).toBe('HR'); + expect(role.description).toBe('Human resources'); + expect(role.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(role.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('normalises missing description to null', () => { + const role = new RoleDomain({ + code: IdentityRole.ADMIN, + title: 'Admin', + }); + + expect(role.description).toBeNull(); + }); + + it('preserves explicit null description', () => { + const role = new RoleDomain({ + code: IdentityRole.EMPLOYEE, + title: 'Employee', + description: null, + }); + + expect(role.description).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/domain/user.domain.spec.ts b/apps/api/test/unit/contexts/identity/domain/user.domain.spec.ts new file mode 100644 index 00000000..48e651f6 --- /dev/null +++ b/apps/api/test/unit/contexts/identity/domain/user.domain.spec.ts @@ -0,0 +1,111 @@ +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; + +describe('UserDomain', () => { + const baseProps = { + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + }; + + describe('create', () => { + it('creates a user with every supplied property', () => { + const user = UserDomain.create({ + id: 1, + ...baseProps, + secondName: 'M.', + fullName: 'Custom Full Name', + avatarUrl: 'https://example.com/avatar.png', + status: IdentityStatus.INACTIVE, + positionId: 3, + teamId: 4, + managerId: 5, + roles: [IdentityRole.HR, IdentityRole.MANAGER], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(user.id).toBe(1); + expect(user.firstName).toBe('Jane'); + expect(user.secondName).toBe('M.'); + expect(user.lastName).toBe('Doe'); + expect(user.fullName).toBe('Custom Full Name'); + expect(user.email).toBe('jane@example.com'); + expect(user.avatarUrl).toBe('https://example.com/avatar.png'); + expect(user.status).toBe(IdentityStatus.INACTIVE); + expect(user.positionId).toBe(3); + expect(user.teamId).toBe(4); + expect(user.managerId).toBe(5); + expect(user.roles).toEqual([IdentityRole.HR, IdentityRole.MANAGER]); + expect(user.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(user.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('builds fullName from first/last when secondName is missing', () => { + const user = UserDomain.create(baseProps); + expect(user.fullName).toBe('Jane Doe'); + }); + + it('builds fullName including secondName when supplied', () => { + const user = UserDomain.create({ + ...baseProps, + secondName: 'M.', + }); + expect(user.fullName).toBe('Jane M. Doe'); + }); + + it('defaults status to ACTIVE, avatarUrl/positionId/teamId/managerId to null, roles to []', () => { + const user = UserDomain.create(baseProps); + expect(user.status).toBe(IdentityStatus.ACTIVE); + expect(user.avatarUrl).toBeNull(); + expect(user.positionId).toBeNull(); + expect(user.teamId).toBeNull(); + expect(user.managerId).toBeNull(); + expect(user.roles).toEqual([]); + }); + }); + + describe('withRoles', () => { + it('returns a new instance with the supplied roles', () => { + const user = UserDomain.create(baseProps); + const next = user.withRoles([IdentityRole.ADMIN]); + + expect(next).not.toBe(user); + expect(next.roles).toEqual([IdentityRole.ADMIN]); + expect(user.roles).toEqual([]); + }); + }); + + describe('withPatch', () => { + it('returns a new instance with the supplied patch applied', () => { + const user = UserDomain.create({ + ...baseProps, + id: 1, + positionId: 10, + }); + const next = user.withPatch({ positionId: 20, teamId: 30 }); + + expect(next).not.toBe(user); + expect(next.positionId).toBe(20); + expect(next.teamId).toBe(30); + expect(user.positionId).toBe(10); + }); + + it('rebuilds fullName when name parts are patched (via default fallback)', () => { + const user = UserDomain.create({ + ...baseProps, + fullName: 'Jane Doe', + }); + const next = user.withPatch({ + firstName: 'Jenny', + fullName: undefined, + }); + // fullName falls back to buildFullName(firstName, secondName, lastName) + expect(next.fullName).toBe('Jenny Doe'); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/infrastructure/mappers/role.mapper.spec.ts b/apps/api/test/unit/contexts/identity/infrastructure/mappers/role.mapper.spec.ts new file mode 100644 index 00000000..bf9d26ca --- /dev/null +++ b/apps/api/test/unit/contexts/identity/infrastructure/mappers/role.mapper.spec.ts @@ -0,0 +1,36 @@ +import { Role as PrismaRole } from '@intra/database'; +import { IdentityRole } from '@intra/shared-kernel'; +import { RoleDomain } from 'src/contexts/identity/domain/role.domain'; +import { RoleMapper } from 'src/contexts/identity/infrastructure/mappers/role.mapper'; + +describe('RoleMapper', () => { + const prismaRole = { + id: 1, + code: 'HR' as PrismaRole['code'], + title: 'HR', + description: 'Human resources', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaRole; + + describe('toDomain', () => { + it('converts a prisma role into a RoleDomain', () => { + const domain = RoleMapper.toDomain(prismaRole); + + expect(domain).toBeInstanceOf(RoleDomain); + expect(domain.id).toBe(1); + expect(domain.code).toBe(IdentityRole.HR); + expect(domain.title).toBe('HR'); + expect(domain.description).toBe('Human resources'); + }); + }); + + describe('toPrismaRole / fromPrismaRole', () => { + it('uppercases the role code in both directions', () => { + expect(RoleMapper.toPrismaRole(IdentityRole.ADMIN)).toBe('ADMIN'); + expect( + RoleMapper.fromPrismaRole('EMPLOYEE' as PrismaRole['code']), + ).toBe(IdentityRole.EMPLOYEE); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/infrastructure/mappers/user.mapper.spec.ts b/apps/api/test/unit/contexts/identity/infrastructure/mappers/user.mapper.spec.ts new file mode 100644 index 00000000..0299da2a --- /dev/null +++ b/apps/api/test/unit/contexts/identity/infrastructure/mappers/user.mapper.spec.ts @@ -0,0 +1,110 @@ +import { User as PrismaUser } from '@intra/database'; +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { UserMapper } from 'src/contexts/identity/infrastructure/mappers/user.mapper'; + +describe('UserMapper', () => { + const prismaUser = { + id: 1, + firstName: 'Jane', + secondName: 'M.', + lastName: 'Doe', + fullName: 'Jane M. Doe', + email: 'jane@example.com', + avatarUrl: 'https://example.com/a.png', + status: 'ACTIVE' as PrismaUser['status'], + positionId: 3, + teamId: 4, + managerId: 5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaUser; + + describe('toDomain', () => { + it('converts a prisma user into a UserDomain with empty roles when none are linked', () => { + const domain = UserMapper.toDomain(prismaUser); + + expect(domain).toBeInstanceOf(UserDomain); + expect(domain.id).toBe(1); + expect(domain.firstName).toBe('Jane'); + expect(domain.secondName).toBe('M.'); + expect(domain.lastName).toBe('Doe'); + expect(domain.fullName).toBe('Jane M. Doe'); + expect(domain.status).toBe(IdentityStatus.ACTIVE); + expect(domain.positionId).toBe(3); + expect(domain.teamId).toBe(4); + expect(domain.managerId).toBe(5); + expect(domain.roles).toEqual([]); + }); + + it('converts userRoles relations into IdentityRole values', () => { + const domain = UserMapper.toDomain({ + ...prismaUser, + userRoles: [{ roleCode: 'HR' }, { roleCode: 'ADMIN' }], + } as any); + + expect(domain.roles).toEqual([IdentityRole.HR, IdentityRole.ADMIN]); + }); + + it('converts null secondName to undefined on the domain side', () => { + const domain = UserMapper.toDomain({ + ...prismaUser, + secondName: null, + } as unknown as PrismaUser); + + expect(domain.secondName).toBeUndefined(); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain without roles into a Prisma create input', () => { + const domain = UserDomain.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + }); + + const prisma = UserMapper.toPrisma(domain); + + expect(prisma.firstName).toBe('Jane'); + expect(prisma.lastName).toBe('Doe'); + expect(prisma.email).toBe('jane@example.com'); + expect(prisma.status).toBe('ACTIVE'); + expect(prisma.positionId).toBeNull(); + expect(prisma.teamId).toBeNull(); + expect(prisma.managerId).toBeNull(); + expect((prisma as any).userRoles).toBeUndefined(); + }); + + it('emits a userRoles.createMany block when roles are present', () => { + const domain = UserDomain.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + roles: [IdentityRole.HR, IdentityRole.MANAGER], + }); + + const prisma = UserMapper.toPrisma(domain); + + expect(prisma.userRoles).toEqual({ + createMany: { + data: [ + { roleCode: IdentityRole.HR }, + { roleCode: IdentityRole.MANAGER }, + ], + }, + }); + }); + }); + + describe('status conversions', () => { + it('uppercases identity status in both directions', () => { + expect(UserMapper.toPrismaStatus(IdentityStatus.ACTIVE)).toBe( + 'ACTIVE', + ); + expect( + UserMapper.toDomainStatus('INACTIVE' as PrismaUser['status']), + ).toBe(IdentityStatus.INACTIVE); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/presentation/controllers/identity-roles.controller.spec.ts b/apps/api/test/unit/contexts/identity/presentation/controllers/identity-roles.controller.spec.ts new file mode 100644 index 00000000..244f37ff --- /dev/null +++ b/apps/api/test/unit/contexts/identity/presentation/controllers/identity-roles.controller.spec.ts @@ -0,0 +1,51 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { IdentityRole } from '@intra/shared-kernel'; +import { IdentityRoleService } from 'src/contexts/identity/application/services/identity-role.service'; +import { RoleDomain } from 'src/contexts/identity/domain/role.domain'; +import { IdentityRolesController } from 'src/contexts/identity/presentation/http/controllers/identity-roles.controller'; + +describe('IdentityRolesController', () => { + let controller: IdentityRolesController; + let service: any; + + beforeEach(() => { + service = { list: jest.fn() }; + + controller = new IdentityRolesController( + service as unknown as IdentityRoleService, + ); + }); + + describe('list', () => { + it('maps every role onto a RoleResponse', async () => { + service.list.mockResolvedValue([ + new RoleDomain({ + id: 1, + code: IdentityRole.HR, + title: 'HR', + description: 'Human resources', + }), + new RoleDomain({ + id: 2, + code: IdentityRole.ADMIN, + title: 'Admin', + description: null, + }), + ]); + + const result = await controller.list(); + + expect(service.list).toHaveBeenCalledTimes(1); + expect(result).toHaveLength(2); + expect(result[0].code).toBe(IdentityRole.HR); + expect(result[0].title).toBe('HR'); + expect(result[0].description).toBe('Human resources'); + expect(result[1].code).toBe(IdentityRole.ADMIN); + expect(result[1].description).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/presentation/controllers/identity-users.controller.spec.ts b/apps/api/test/unit/contexts/identity/presentation/controllers/identity-users.controller.spec.ts new file mode 100644 index 00000000..2c674629 --- /dev/null +++ b/apps/api/test/unit/contexts/identity/presentation/controllers/identity-users.controller.spec.ts @@ -0,0 +1,147 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { IdentityUsersController } from 'src/contexts/identity/presentation/http/controllers/identity-users.controller'; + +function buildUser(): UserDomain { + return UserDomain.create({ + id: 1, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + roles: [IdentityRole.HR], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +describe('IdentityUsersController', () => { + let controller: IdentityUsersController; + let service: any; + + beforeEach(() => { + service = { + create: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + replaceRoles: jest.fn(), + delete: jest.fn(), + }; + + controller = new IdentityUsersController( + service as unknown as IdentityUserService, + ); + }); + + describe('create', () => { + it('forwards the dto to the service and maps the response', async () => { + service.create.mockResolvedValue(buildUser()); + + const result = await controller.create({ + firstName: 'Jane', + secondName: 'M.', + lastName: 'Doe', + fullName: 'Jane M. Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + positionId: 3, + teamId: 4, + managerId: 5, + roles: [IdentityRole.HR], + } as any); + + expect(service.create).toHaveBeenCalledWith({ + firstName: 'Jane', + secondName: 'M.', + lastName: 'Doe', + fullName: 'Jane M. Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + positionId: 3, + teamId: 4, + managerId: 5, + roles: [IdentityRole.HR], + }); + expect(result.id).toBe(1); + }); + }); + + describe('search', () => { + it('maps every user to a response', async () => { + service.search.mockResolvedValue([buildUser()]); + + const result = await controller.search({} as any); + + expect(service.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('parses the id and requests roles inclusion', async () => { + service.getById.mockResolvedValue(buildUser()); + + const result = await controller.getById('1'); + + expect(service.getById).toHaveBeenCalledWith(1, { + withRoles: true, + }); + expect(result.id).toBe(1); + }); + }); + + describe('update', () => { + it('parses the id and forwards the patch', async () => { + service.update.mockResolvedValue(buildUser()); + + await controller.update('1', { + firstName: 'Jenny', + lastName: 'Doe', + status: IdentityStatus.INACTIVE, + positionId: 3, + teamId: 4, + managerId: 5, + } as any); + + expect(service.update).toHaveBeenCalledWith(1, { + firstName: 'Jenny', + secondName: undefined, + lastName: 'Doe', + status: IdentityStatus.INACTIVE, + positionId: 3, + teamId: 4, + managerId: 5, + }); + }); + }); + + describe('replaceRoles', () => { + it('parses the id and forwards the roles list', async () => { + service.replaceRoles.mockResolvedValue(buildUser()); + + const result = await controller.replaceRoles('1', { + roles: [IdentityRole.HR, IdentityRole.MANAGER], + } as any); + + expect(service.replaceRoles).toHaveBeenCalledWith(1, [ + IdentityRole.HR, + IdentityRole.MANAGER, + ]); + expect(result.id).toBe(1); + }); + }); + + describe('delete', () => { + it('parses the id and calls service.delete', async () => { + await controller.delete('1'); + expect(service.delete).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/identity/presentation/mappers/user.http.mapper.spec.ts b/apps/api/test/unit/contexts/identity/presentation/mappers/user.http.mapper.spec.ts new file mode 100644 index 00000000..1f9e0b9f --- /dev/null +++ b/apps/api/test/unit/contexts/identity/presentation/mappers/user.http.mapper.spec.ts @@ -0,0 +1,65 @@ +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { UserHttpMapper } from 'src/contexts/identity/presentation/http/mappers/user.http.mapper'; + +describe('UserHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = UserDomain.create({ + id: 1, + firstName: 'Jane', + secondName: 'M.', + lastName: 'Doe', + fullName: 'Jane M. Doe', + email: 'jane@example.com', + avatarUrl: 'https://example.com/a.png', + status: IdentityStatus.ACTIVE, + positionId: 3, + teamId: 4, + managerId: 5, + roles: [IdentityRole.HR], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = UserHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.firstName).toBe('Jane'); + expect(response.secondName).toBe('M.'); + expect(response.lastName).toBe('Doe'); + expect(response.fullName).toBe('Jane M. Doe'); + expect(response.email).toBe('jane@example.com'); + expect(response.avatarUrl).toBe('https://example.com/a.png'); + expect(response.status).toBe(IdentityStatus.ACTIVE); + expect(response.positionId).toBe(3); + expect(response.teamId).toBe(4); + expect(response.managerId).toBe(5); + expect(response.roles).toEqual([IdentityRole.HR]); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('returns null for missing optional fields and [] for roles', () => { + const domain = UserDomain.create({ + id: 1, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + }); + + const response = UserHttpMapper.toResponse(domain); + + expect(response.secondName).toBeNull(); + expect(response.avatarUrl).toBeNull(); + expect(response.positionId).toBeNull(); + expect(response.teamId).toBeNull(); + expect(response.managerId).toBeNull(); + expect(response.roles).toEqual([]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/application/services/cluster.service.spec.ts b/apps/api/test/unit/contexts/library/application/services/cluster.service.spec.ts new file mode 100644 index 00000000..4c49bcfc --- /dev/null +++ b/apps/api/test/unit/contexts/library/application/services/cluster.service.spec.ts @@ -0,0 +1,179 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { CLUSTER_REPOSITORY } from 'src/contexts/library/application/ports/cluster.repository.port'; +import { ClusterService } from 'src/contexts/library/application/services/cluster.service'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; + +function buildCluster( + overrides: Partial[0]> = {}, +): ClusterDomain { + return ClusterDomain.create({ + id: 1, + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'Top performer', + description: 'Above 4.5', + ...overrides, + }); +} + +describe('ClusterService', () => { + let service: ClusterService; + let clusters: any; + let competences: any; + + beforeEach(async () => { + clusters = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + }; + competences = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + ClusterService, + { provide: CLUSTER_REPOSITORY, useValue: clusters }, + { provide: CompetenceService, useValue: competences }, + ], + }).compile(); + + service = module.get(ClusterService); + }); + + describe('create', () => { + it('rejects when lowerBound > upperBound', async () => { + await expect( + service.create({ + competenceId: 7, + lowerBound: 5, + upperBound: 0, + title: 'X', + description: 'Y', + }), + ).rejects.toBeInstanceOf(BadRequestException); + expect(clusters.create).not.toHaveBeenCalled(); + }); + + it('verifies the competence exists before persisting', async () => { + competences.getById.mockResolvedValue({ id: 7 }); + clusters.create.mockImplementation(async (c: ClusterDomain) => c); + + await service.create({ + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'X', + description: 'Y', + }); + + expect(competences.getById).toHaveBeenCalledWith(7); + expect(clusters.create).toHaveBeenCalledTimes(1); + const passed = clusters.create.mock.calls[0][0]; + expect(passed).toBeInstanceOf(ClusterDomain); + expect(passed.competenceId).toBe(7); + }); + }); + + describe('search', () => { + it('delegates to the repository', async () => { + const result = [buildCluster()]; + clusters.search.mockResolvedValue(result); + + const out = await service.search({}); + expect(clusters.search).toHaveBeenCalledWith({}); + expect(out).toBe(result); + }); + }); + + describe('getById', () => { + it('returns the cluster when found', async () => { + const cluster = buildCluster(); + clusters.findById.mockResolvedValue(cluster); + await expect(service.getById(1)).resolves.toBe(cluster); + }); + + it('throws NotFoundException when missing', async () => { + clusters.findById.mockResolvedValue(null); + await expect(service.getById(99)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('throws NotFoundException when the cluster does not exist', async () => { + clusters.findById.mockResolvedValue(null); + await expect( + service.update(1, { title: 'X' }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('verifies a new competence id before updating', async () => { + clusters.findById.mockResolvedValue( + buildCluster({ competenceId: 7 }), + ); + competences.getById.mockResolvedValue({ id: 9 }); + clusters.updateById.mockResolvedValue(buildCluster()); + + await service.update(1, { competenceId: 9 }); + + expect(competences.getById).toHaveBeenCalledWith(9); + expect(clusters.updateById).toHaveBeenCalledWith(1, { + competenceId: 9, + }); + }); + + it('skips competence verification when the id does not change', async () => { + clusters.findById.mockResolvedValue( + buildCluster({ competenceId: 7 }), + ); + clusters.updateById.mockResolvedValue(buildCluster()); + + await service.update(1, { competenceId: 7 }); + + expect(competences.getById).not.toHaveBeenCalled(); + }); + + it('validates merged bounds against current values', async () => { + clusters.findById.mockResolvedValue( + buildCluster({ lowerBound: 1, upperBound: 4 }), + ); + + await expect( + service.update(1, { lowerBound: 5 }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('forwards only defined patch fields to the repository', async () => { + clusters.findById.mockResolvedValue(buildCluster()); + clusters.updateById.mockResolvedValue(buildCluster()); + + await service.update(1, { title: 'New title' }); + + expect(clusters.updateById).toHaveBeenCalledWith(1, { + title: 'New title', + }); + }); + }); + + describe('delete', () => { + it('throws when the cluster does not exist', async () => { + clusters.findById.mockResolvedValue(null); + await expect(service.delete(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + expect(clusters.deleteById).not.toHaveBeenCalled(); + }); + + it('deletes when found', async () => { + clusters.findById.mockResolvedValue(buildCluster()); + await service.delete(1); + expect(clusters.deleteById).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/application/services/competence.service.spec.ts b/apps/api/test/unit/contexts/library/application/services/competence.service.spec.ts new file mode 100644 index 00000000..00ab3ff0 --- /dev/null +++ b/apps/api/test/unit/contexts/library/application/services/competence.service.spec.ts @@ -0,0 +1,282 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/competence-question-template-relation.repository.port'; +import { COMPETENCE_REPOSITORY } from 'src/contexts/library/application/ports/competence.repository.port'; +import { POSITION_COMPETENCE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-competence-relation.repository.port'; +import { QUESTION_TEMPLATE_REPOSITORY } from 'src/contexts/library/application/ports/question-template.repository.port'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; + +function buildCompetence( + overrides: Partial[0]> = {}, +): CompetenceDomain { + return CompetenceDomain.create({ + id: 1, + title: 'Teamwork', + ...overrides, + }); +} + +describe('CompetenceService', () => { + let service: CompetenceService; + let competences: any; + let questionTemplates: any; + let positionCompetenceRelations: any; + let competenceQuestionTemplateRelations: any; + let positions: any; + + beforeEach(async () => { + competences = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + }; + questionTemplates = { findById: jest.fn() }; + positionCompetenceRelations = { + link: jest.fn(), + unlink: jest.fn(), + listByCompetence: jest.fn(), + deleteAllForCompetence: jest.fn(), + }; + competenceQuestionTemplateRelations = { + link: jest.fn(), + unlink: jest.fn(), + listByCompetence: jest.fn(), + deleteAllForCompetence: jest.fn(), + }; + positions = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + CompetenceService, + { provide: COMPETENCE_REPOSITORY, useValue: competences }, + { + provide: QUESTION_TEMPLATE_REPOSITORY, + useValue: questionTemplates, + }, + { + provide: POSITION_COMPETENCE_RELATION_REPOSITORY, + useValue: positionCompetenceRelations, + }, + { + provide: COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useValue: competenceQuestionTemplateRelations, + }, + { provide: PositionService, useValue: positions }, + ], + }).compile(); + + service = module.get(CompetenceService); + }); + + describe('create', () => { + it('persists the competence with null fallbacks for code and description', async () => { + competences.create.mockImplementation( + async (c: CompetenceDomain) => c, + ); + + await service.create({ title: 'Teamwork' }); + + const passed = competences.create.mock.calls[0][0]; + expect(passed).toBeInstanceOf(CompetenceDomain); + expect(passed.code).toBeNull(); + expect(passed.description).toBeNull(); + }); + + it('passes through optional fields', async () => { + competences.create.mockImplementation( + async (c: CompetenceDomain) => c, + ); + + await service.create({ + title: 'Teamwork', + code: 'TWK', + description: 'desc', + }); + + const passed = competences.create.mock.calls[0][0]; + expect(passed.code).toBe('TWK'); + expect(passed.description).toBe('desc'); + }); + }); + + describe('search', () => { + it('delegates to the repository', async () => { + competences.search.mockResolvedValue([]); + await service.search({}); + expect(competences.search).toHaveBeenCalledWith({}); + }); + }); + + describe('getById', () => { + it('returns the competence when found', async () => { + const competence = buildCompetence(); + competences.findById.mockResolvedValue(competence); + await expect(service.getById(1)).resolves.toBe(competence); + }); + + it('throws NotFoundException when missing', async () => { + competences.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('throws NotFoundException when the competence does not exist', async () => { + competences.findById.mockResolvedValue(null); + await expect( + service.update(1, { title: 'X' }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('forwards only defined patch fields to the repository', async () => { + competences.findById.mockResolvedValue(buildCompetence()); + competences.updateById.mockResolvedValue(buildCompetence()); + + await service.update(1, { title: 'X' }); + + expect(competences.updateById).toHaveBeenCalledWith(1, { + title: 'X', + }); + }); + }); + + describe('delete', () => { + it('rejects when related question templates exist', async () => { + competences.findById.mockResolvedValue(buildCompetence()); + competenceQuestionTemplateRelations.listByCompetence.mockResolvedValue( + [{ competenceId: 1, questionTemplateId: 5 }], + ); + positionCompetenceRelations.listByCompetence.mockResolvedValue([]); + + await expect(service.delete(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(competences.deleteById).not.toHaveBeenCalled(); + }); + + it('rejects when related positions exist', async () => { + competences.findById.mockResolvedValue(buildCompetence()); + competenceQuestionTemplateRelations.listByCompetence.mockResolvedValue( + [], + ); + positionCompetenceRelations.listByCompetence.mockResolvedValue([ + { competenceId: 1, positionId: 5 }, + ]); + + await expect(service.delete(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it('cleans up relations and deletes the competence when nothing is linked', async () => { + competences.findById.mockResolvedValue(buildCompetence()); + competenceQuestionTemplateRelations.listByCompetence.mockResolvedValue( + [], + ); + positionCompetenceRelations.listByCompetence.mockResolvedValue([]); + + await service.delete(1); + + expect( + positionCompetenceRelations.deleteAllForCompetence, + ).toHaveBeenCalledWith(1); + expect( + competenceQuestionTemplateRelations.deleteAllForCompetence, + ).toHaveBeenCalledWith(1); + expect(competences.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('attachPosition / detachPosition / listPositions', () => { + beforeEach(() => { + competences.findById.mockResolvedValue(buildCompetence()); + }); + + it('attachPosition links the relation and returns updated position ids', async () => { + positions.getById.mockResolvedValue({ id: 11 }); + positionCompetenceRelations.listByCompetence.mockResolvedValue([ + { competenceId: 1, positionId: 11 }, + ]); + + const result = await service.attachPosition(1, 11); + + expect(positions.getById).toHaveBeenCalledWith(11); + expect(positionCompetenceRelations.link).toHaveBeenCalledWith( + 1, + 11, + ); + expect(result).toEqual([11]); + }); + + it('detachPosition unlinks the relation', async () => { + await service.detachPosition(1, 11); + expect(positionCompetenceRelations.unlink).toHaveBeenCalledWith( + 1, + 11, + ); + }); + + it('listPositions maps relations to position ids', async () => { + positionCompetenceRelations.listByCompetence.mockResolvedValue([ + { positionId: 11 }, + { positionId: 12 }, + ]); + + const result = await service.listPositions(1); + expect(result).toEqual([11, 12]); + }); + }); + + describe('attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates', () => { + beforeEach(() => { + competences.findById.mockResolvedValue(buildCompetence()); + }); + + it('throws NotFoundException when the question template does not exist', async () => { + questionTemplates.findById.mockResolvedValue(null); + + await expect( + service.attachQuestionTemplate(1, 50), + ).rejects.toBeInstanceOf(NotFoundException); + expect( + competenceQuestionTemplateRelations.link, + ).not.toHaveBeenCalled(); + }); + + it('attachQuestionTemplate links and returns the updated ids', async () => { + questionTemplates.findById.mockResolvedValue({ id: 50 }); + competenceQuestionTemplateRelations.listByCompetence.mockResolvedValue( + [{ questionTemplateId: 50 }], + ); + + const result = await service.attachQuestionTemplate(1, 50); + + expect( + competenceQuestionTemplateRelations.link, + ).toHaveBeenCalledWith(1, 50); + expect(result).toEqual([50]); + }); + + it('detachQuestionTemplate unlinks the relation', async () => { + await service.detachQuestionTemplate(1, 50); + expect( + competenceQuestionTemplateRelations.unlink, + ).toHaveBeenCalledWith(1, 50); + }); + + it('listQuestionTemplates maps relations to question template ids', async () => { + competenceQuestionTemplateRelations.listByCompetence.mockResolvedValue( + [{ questionTemplateId: 50 }, { questionTemplateId: 51 }], + ); + + const result = await service.listQuestionTemplates(1); + expect(result).toEqual([50, 51]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/application/services/question-template.service.spec.ts b/apps/api/test/unit/contexts/library/application/services/question-template.service.spec.ts new file mode 100644 index 00000000..32c8f60b --- /dev/null +++ b/apps/api/test/unit/contexts/library/application/services/question-template.service.spec.ts @@ -0,0 +1,271 @@ +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { QUESTION_REPOSITORY } from 'src/contexts/feedback360/application/ports/question.repository.port'; +import { COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/competence-question-template-relation.repository.port'; +import { POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-question-template-relation.repository.port'; +import { QUESTION_TEMPLATE_REPOSITORY } from 'src/contexts/library/application/ports/question-template.repository.port'; +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; + +function buildTemplate( + overrides: Partial< + Parameters[0] + > = {}, +): QuestionTemplateDomain { + return QuestionTemplateDomain.create({ + id: 1, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 7, + ...overrides, + }); +} + +describe('QuestionTemplateService', () => { + let service: QuestionTemplateService; + let templates: any; + let positionRelations: any; + let competenceRelations: any; + let competences: any; + let positions: any; + let reviewQuestions: any; + + beforeEach(async () => { + templates = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + }; + positionRelations = { + replace: jest.fn(), + link: jest.fn(), + unlink: jest.fn(), + listByQuestion: jest.fn(), + deleteAllForQuestionTemplate: jest.fn(), + }; + competenceRelations = { + link: jest.fn(), + unlink: jest.fn(), + listByQuestionTemplate: jest.fn(), + deleteAllForQuestionTemplate: jest.fn(), + }; + competences = { getById: jest.fn() }; + positions = { getById: jest.fn() }; + reviewQuestions = { search: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + QuestionTemplateService, + { provide: QUESTION_TEMPLATE_REPOSITORY, useValue: templates }, + { + provide: POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useValue: positionRelations, + }, + { + provide: COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY, + useValue: competenceRelations, + }, + { provide: CompetenceService, useValue: competences }, + { provide: PositionService, useValue: positions }, + { provide: QUESTION_REPOSITORY, useValue: reviewQuestions }, + ], + }).compile(); + + service = module.get(QuestionTemplateService); + }); + + describe('create', () => { + it('verifies the competence exists and persists the template', async () => { + competences.getById.mockResolvedValue({ id: 7 }); + const created = buildTemplate(); + templates.create.mockResolvedValue(created); + templates.findById.mockResolvedValue(created); + + const result = await service.create({ + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 7, + }); + + expect(competences.getById).toHaveBeenCalledWith(7); + const passed = templates.create.mock.calls[0][0]; + expect(passed).toBeInstanceOf(QuestionTemplateDomain); + expect(passed.status).toBe(QuestionTemplateStatus.ACTIVE); + expect(passed.positionIds).toEqual([]); + expect(result).toBe(created); + }); + + it('replaces position relations after creation when positionIds are supplied', async () => { + competences.getById.mockResolvedValue({ id: 7 }); + const created = buildTemplate({ id: 42 }); + templates.create.mockResolvedValue(created); + templates.findById.mockResolvedValue(created); + positions.getById.mockResolvedValue({ id: 1 }); + + await service.create({ + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 7, + positionIds: [1, 1, 2], // duplicates should be deduped + }); + + expect(positions.getById).toHaveBeenCalledTimes(2); + expect(positionRelations.replace).toHaveBeenCalledWith(42, [1, 2]); + }); + }); + + describe('search', () => { + it('delegates to the repository', async () => { + templates.search.mockResolvedValue([]); + await service.search({}); + expect(templates.search).toHaveBeenCalledWith({}); + }); + }); + + describe('getById', () => { + it('returns the template when found', async () => { + const template = buildTemplate(); + templates.findById.mockResolvedValue(template); + await expect(service.getById(1)).resolves.toBe(template); + }); + + it('throws NotFoundException when missing', async () => { + templates.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('verifies a new competence id when one is supplied', async () => { + templates.findById.mockResolvedValue( + buildTemplate({ competenceId: 7 }), + ); + competences.getById.mockResolvedValue({ id: 9 }); + templates.updateById.mockResolvedValue(undefined); + + await service.update(1, { competenceId: 9 }); + + expect(competences.getById).toHaveBeenCalledWith(9); + }); + + it('replaces position relations when positionIds are supplied', async () => { + templates.findById.mockResolvedValue(buildTemplate({ id: 1 })); + positions.getById.mockResolvedValue({ id: 3 }); + + await service.update(1, { positionIds: [3, 3, 4] }); + + expect(positionRelations.replace).toHaveBeenCalledWith(1, [3, 4]); + }); + + it('forwards only defined patch fields to the repository', async () => { + templates.findById.mockResolvedValue(buildTemplate()); + + await service.update(1, { title: 'New' }); + + expect(templates.updateById).toHaveBeenCalledWith(1, { + title: 'New', + }); + }); + }); + + describe('delete', () => { + it('rejects when related review questions exist', async () => { + templates.findById.mockResolvedValue(buildTemplate()); + reviewQuestions.search.mockResolvedValue([{ id: 1 }]); + + await expect(service.delete(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(templates.deleteById).not.toHaveBeenCalled(); + }); + + it('cleans up relations and deletes the template when nothing is linked', async () => { + templates.findById.mockResolvedValue(buildTemplate()); + reviewQuestions.search.mockResolvedValue([]); + + await service.delete(1); + + expect( + positionRelations.deleteAllForQuestionTemplate, + ).toHaveBeenCalledWith(1); + expect( + competenceRelations.deleteAllForQuestionTemplate, + ).toHaveBeenCalledWith(1); + expect(templates.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('attachPosition / detachPosition / listPositions', () => { + beforeEach(() => { + templates.findById.mockResolvedValue(buildTemplate()); + }); + + it('attachPosition links the relation and returns updated ids', async () => { + positions.getById.mockResolvedValue({ id: 11 }); + positionRelations.listByQuestion.mockResolvedValue([ + { positionId: 11 }, + ]); + + const result = await service.attachPosition(1, 11); + + expect(positionRelations.link).toHaveBeenCalledWith(1, 11); + expect(result).toEqual([11]); + }); + + it('detachPosition unlinks the relation', async () => { + await service.detachPosition(1, 11); + expect(positionRelations.unlink).toHaveBeenCalledWith(1, 11); + }); + + it('listPositions maps relations to position ids', async () => { + positionRelations.listByQuestion.mockResolvedValue([ + { positionId: 11 }, + { positionId: 12 }, + ]); + + const result = await service.listPositions(1); + expect(result).toEqual([11, 12]); + }); + }); + + describe('attachCompetence / detachCompetence / listCompetences', () => { + beforeEach(() => { + templates.findById.mockResolvedValue(buildTemplate()); + }); + + it('attachCompetence verifies the competence and links the relation', async () => { + competences.getById.mockResolvedValue({ id: 7 }); + competenceRelations.listByQuestionTemplate.mockResolvedValue([ + { competenceId: 7 }, + ]); + + const result = await service.attachCompetence(1, 7); + + expect(competences.getById).toHaveBeenCalledWith(7); + expect(competenceRelations.link).toHaveBeenCalledWith(7, 1); + expect(result).toEqual([7]); + }); + + it('detachCompetence unlinks the relation', async () => { + await service.detachCompetence(1, 7); + expect(competenceRelations.unlink).toHaveBeenCalledWith(7, 1); + }); + + it('listCompetences maps relations to competence ids', async () => { + competenceRelations.listByQuestionTemplate.mockResolvedValue([ + { competenceId: 7 }, + { competenceId: 8 }, + ]); + + const result = await service.listCompetences(1); + expect(result).toEqual([7, 8]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/domain/cluster.domain.spec.ts b/apps/api/test/unit/contexts/library/domain/cluster.domain.spec.ts new file mode 100644 index 00000000..02318246 --- /dev/null +++ b/apps/api/test/unit/contexts/library/domain/cluster.domain.spec.ts @@ -0,0 +1,43 @@ +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; + +describe('ClusterDomain', () => { + const baseProps = { + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'Top performer', + description: 'Above 4.5', + }; + + describe('create', () => { + it('creates a cluster with every supplied property', () => { + const cluster = ClusterDomain.create({ + id: 1, + ...baseProps, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(cluster.id).toBe(1); + expect(cluster.competenceId).toBe(7); + expect(cluster.lowerBound).toBe(0); + expect(cluster.upperBound).toBe(5); + expect(cluster.title).toBe('Top performer'); + expect(cluster.description).toBe('Above 4.5'); + expect(cluster.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(cluster.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('leaves id, createdAt, updatedAt undefined when not provided', () => { + const cluster = ClusterDomain.create(baseProps); + + expect(cluster.id).toBeUndefined(); + expect(cluster.createdAt).toBeUndefined(); + expect(cluster.updatedAt).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/domain/competence-question-template-relation.domain.spec.ts b/apps/api/test/unit/contexts/library/domain/competence-question-template-relation.domain.spec.ts new file mode 100644 index 00000000..e8684cfb --- /dev/null +++ b/apps/api/test/unit/contexts/library/domain/competence-question-template-relation.domain.spec.ts @@ -0,0 +1,31 @@ +import { CompetenceQuestionTemplateRelationDomain } from 'src/contexts/library/domain/competence-question-template-relation.domain'; + +describe('CompetenceQuestionTemplateRelationDomain', () => { + describe('create', () => { + it('creates a relation with every supplied field', () => { + const relation = CompetenceQuestionTemplateRelationDomain.create({ + id: 1, + competenceId: 7, + questionTemplateId: 50, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(relation.id).toBe(1); + expect(relation.competenceId).toBe(7); + expect(relation.questionTemplateId).toBe(50); + expect(relation.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('omits id and createdAt when not provided', () => { + const relation = CompetenceQuestionTemplateRelationDomain.create({ + competenceId: 7, + questionTemplateId: 50, + }); + + expect(relation.id).toBeUndefined(); + expect(relation.createdAt).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/domain/competence.domain.spec.ts b/apps/api/test/unit/contexts/library/domain/competence.domain.spec.ts new file mode 100644 index 00000000..60dc702c --- /dev/null +++ b/apps/api/test/unit/contexts/library/domain/competence.domain.spec.ts @@ -0,0 +1,49 @@ +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; + +describe('CompetenceDomain', () => { + const baseProps = { + title: 'Teamwork', + }; + + describe('create', () => { + it('creates a competence with every supplied property', () => { + const competence = CompetenceDomain.create({ + id: 1, + code: 'TWK', + title: 'Teamwork', + description: 'Working with peers', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(competence.id).toBe(1); + expect(competence.code).toBe('TWK'); + expect(competence.title).toBe('Teamwork'); + expect(competence.description).toBe('Working with peers'); + expect(competence.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(competence.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('normalises missing code and description to null', () => { + const competence = CompetenceDomain.create(baseProps); + + expect(competence.code).toBeNull(); + expect(competence.description).toBeNull(); + }); + + it('preserves explicit null code and description', () => { + const competence = CompetenceDomain.create({ + ...baseProps, + code: null, + description: null, + }); + + expect(competence.code).toBeNull(); + expect(competence.description).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/domain/position-competence-relation.domain.spec.ts b/apps/api/test/unit/contexts/library/domain/position-competence-relation.domain.spec.ts new file mode 100644 index 00000000..bcab31a9 --- /dev/null +++ b/apps/api/test/unit/contexts/library/domain/position-competence-relation.domain.spec.ts @@ -0,0 +1,31 @@ +import { PositionCompetenceRelationDomain } from 'src/contexts/library/domain/position-competence-relation.domain'; + +describe('PositionCompetenceRelationDomain', () => { + describe('create', () => { + it('creates a relation with every supplied field', () => { + const relation = PositionCompetenceRelationDomain.create({ + id: 1, + positionId: 11, + competenceId: 7, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(relation.id).toBe(1); + expect(relation.positionId).toBe(11); + expect(relation.competenceId).toBe(7); + expect(relation.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('omits id and createdAt when not provided', () => { + const relation = PositionCompetenceRelationDomain.create({ + positionId: 11, + competenceId: 7, + }); + + expect(relation.id).toBeUndefined(); + expect(relation.createdAt).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/domain/position-question-template-relation.domain.spec.ts b/apps/api/test/unit/contexts/library/domain/position-question-template-relation.domain.spec.ts new file mode 100644 index 00000000..6994f7c2 --- /dev/null +++ b/apps/api/test/unit/contexts/library/domain/position-question-template-relation.domain.spec.ts @@ -0,0 +1,31 @@ +import { PositionQuestionTemplateRelationDomain } from 'src/contexts/library/domain/position-question-template-relation.domain'; + +describe('PositionQuestionTemplateRelationDomain', () => { + describe('create', () => { + it('creates a relation with every supplied field', () => { + const relation = PositionQuestionTemplateRelationDomain.create({ + id: 1, + questionTemplateId: 50, + positionId: 11, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(relation.id).toBe(1); + expect(relation.questionTemplateId).toBe(50); + expect(relation.positionId).toBe(11); + expect(relation.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('omits id and createdAt when not provided', () => { + const relation = PositionQuestionTemplateRelationDomain.create({ + questionTemplateId: 50, + positionId: 11, + }); + + expect(relation.id).toBeUndefined(); + expect(relation.createdAt).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/domain/question-template.domain.spec.ts b/apps/api/test/unit/contexts/library/domain/question-template.domain.spec.ts new file mode 100644 index 00000000..6992367f --- /dev/null +++ b/apps/api/test/unit/contexts/library/domain/question-template.domain.spec.ts @@ -0,0 +1,64 @@ +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; + +describe('QuestionTemplateDomain', () => { + const baseProps = { + title: 'Are you a team player?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 7, + }; + + describe('create', () => { + it('creates a question template with every supplied property', () => { + const template = QuestionTemplateDomain.create({ + id: 1, + ...baseProps, + isForSelfassessment: true, + status: QuestionTemplateStatus.ARCHIVE, + positionIds: [10, 11], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(template.id).toBe(1); + expect(template.title).toBe('Are you a team player?'); + expect(template.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(template.competenceId).toBe(7); + expect(template.isForSelfassessment).toBe(true); + expect(template.status).toBe(QuestionTemplateStatus.ARCHIVE); + expect(template.positionIds).toEqual([10, 11]); + }); + + it('defaults isForSelfassessment to false, status to ACTIVE, positionIds to []', () => { + const template = QuestionTemplateDomain.create(baseProps); + + expect(template.isForSelfassessment).toBe(false); + expect(template.status).toBe(QuestionTemplateStatus.ACTIVE); + expect(template.positionIds).toEqual([]); + }); + + it('coerces null isForSelfassessment to false', () => { + const template = QuestionTemplateDomain.create({ + ...baseProps, + isForSelfassessment: null, + }); + + expect(template.isForSelfassessment).toBe(false); + }); + }); + + describe('withPositions', () => { + it('returns a new instance carrying the supplied positionIds', () => { + const template = QuestionTemplateDomain.create({ + ...baseProps, + positionIds: [1, 2], + }); + + const next = template.withPositions([3, 4, 5]); + + expect(next).not.toBe(template); + expect(next.positionIds).toEqual([3, 4, 5]); + expect(template.positionIds).toEqual([1, 2]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/infrastructure/mappers/cluster.mapper.spec.ts b/apps/api/test/unit/contexts/library/infrastructure/mappers/cluster.mapper.spec.ts new file mode 100644 index 00000000..9ebfa094 --- /dev/null +++ b/apps/api/test/unit/contexts/library/infrastructure/mappers/cluster.mapper.spec.ts @@ -0,0 +1,59 @@ +import { Cluster as PrismaCluster } from '@intra/database'; +import Decimal from 'decimal.js'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { ClusterMapper } from 'src/contexts/library/infrastructure/mappers/cluster.mapper'; + +describe('ClusterMapper', () => { + const prismaCluster = { + id: 1, + competenceId: 7, + lowerBound: new Decimal('0'), + upperBound: new Decimal('5'), + title: 'Top performer', + description: 'Above 4.5', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as unknown as PrismaCluster; + + describe('toDomain', () => { + it('converts a prisma row into a ClusterDomain, coercing decimals to numbers', () => { + const domain = ClusterMapper.toDomain(prismaCluster); + + expect(domain).toBeInstanceOf(ClusterDomain); + expect(domain.id).toBe(1); + expect(domain.competenceId).toBe(7); + expect(domain.lowerBound).toBe(0); + expect(domain.upperBound).toBe(5); + expect(domain.title).toBe('Top performer'); + expect(domain.description).toBe('Above 4.5'); + expect(domain.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(domain.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input', () => { + const domain = ClusterDomain.create({ + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'High', + description: 'Above 4', + }); + + const prisma = ClusterMapper.toPrisma(domain); + + expect(prisma).toEqual({ + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'High', + description: 'Above 4', + }); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/infrastructure/mappers/competence-question-template-relation.mapper.spec.ts b/apps/api/test/unit/contexts/library/infrastructure/mappers/competence-question-template-relation.mapper.spec.ts new file mode 100644 index 00000000..0a5324d4 --- /dev/null +++ b/apps/api/test/unit/contexts/library/infrastructure/mappers/competence-question-template-relation.mapper.spec.ts @@ -0,0 +1,29 @@ +import { CompetenceQuestionTemplateRelation as PrismaCompetenceQuestionTemplateRelation } from '@intra/database'; +import { CompetenceQuestionTemplateRelationDomain } from 'src/contexts/library/domain/competence-question-template-relation.domain'; +import { CompetenceQuestionTemplateRelationMapper } from 'src/contexts/library/infrastructure/mappers/competence-question-template-relation.mapper'; + +describe('CompetenceQuestionTemplateRelationMapper', () => { + const prismaRow = { + id: 1, + competenceId: 7, + questionTemplateId: 50, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaCompetenceQuestionTemplateRelation; + + describe('toDomain', () => { + it('converts a prisma row into a CompetenceQuestionTemplateRelationDomain', () => { + const domain = + CompetenceQuestionTemplateRelationMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf( + CompetenceQuestionTemplateRelationDomain, + ); + expect(domain.id).toBe(1); + expect(domain.competenceId).toBe(7); + expect(domain.questionTemplateId).toBe(50); + expect(domain.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/infrastructure/mappers/competence.mapper.spec.ts b/apps/api/test/unit/contexts/library/infrastructure/mappers/competence.mapper.spec.ts new file mode 100644 index 00000000..a17c96df --- /dev/null +++ b/apps/api/test/unit/contexts/library/infrastructure/mappers/competence.mapper.spec.ts @@ -0,0 +1,66 @@ +import { Competence as PrismaCompetence } from '@intra/database'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceMapper } from 'src/contexts/library/infrastructure/mappers/competence.mapper'; + +describe('CompetenceMapper', () => { + const prismaCompetence = { + id: 1, + code: 'TWK', + title: 'Teamwork', + description: 'Working with peers', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaCompetence; + + describe('toDomain', () => { + it('converts a prisma row into a CompetenceDomain', () => { + const domain = CompetenceMapper.toDomain(prismaCompetence); + + expect(domain).toBeInstanceOf(CompetenceDomain); + expect(domain.id).toBe(1); + expect(domain.code).toBe('TWK'); + expect(domain.title).toBe('Teamwork'); + expect(domain.description).toBe('Working with peers'); + }); + + it('preserves null code/description coming from the database', () => { + const domain = CompetenceMapper.toDomain({ + ...prismaCompetence, + code: null, + description: null, + } as unknown as PrismaCompetence); + + expect(domain.code).toBeNull(); + expect(domain.description).toBeNull(); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input', () => { + const domain = CompetenceDomain.create({ + code: 'TWK', + title: 'Teamwork', + description: 'desc', + }); + + const prisma = CompetenceMapper.toPrisma(domain); + + expect(prisma).toEqual({ + code: 'TWK', + title: 'Teamwork', + description: 'desc', + }); + }); + + it('passes through null code/description when present on the domain', () => { + const domain = CompetenceDomain.create({ + title: 'Teamwork', + }); + + const prisma = CompetenceMapper.toPrisma(domain); + + expect(prisma.code).toBeNull(); + expect(prisma.description).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/infrastructure/mappers/position-competence-relation.mapper.spec.ts b/apps/api/test/unit/contexts/library/infrastructure/mappers/position-competence-relation.mapper.spec.ts new file mode 100644 index 00000000..96e2c5ac --- /dev/null +++ b/apps/api/test/unit/contexts/library/infrastructure/mappers/position-competence-relation.mapper.spec.ts @@ -0,0 +1,26 @@ +import { PositionCompetenceRelation as PrismaPositionCompetenceRelation } from '@intra/database'; +import { PositionCompetenceRelationDomain } from 'src/contexts/library/domain/position-competence-relation.domain'; +import { PositionCompetenceRelationMapper } from 'src/contexts/library/infrastructure/mappers/position-competence-relation.mapper'; + +describe('PositionCompetenceRelationMapper', () => { + const prismaRow = { + id: 1, + positionId: 11, + competenceId: 7, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaPositionCompetenceRelation; + + describe('toDomain', () => { + it('converts a prisma row into a PositionCompetenceRelationDomain', () => { + const domain = PositionCompetenceRelationMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(PositionCompetenceRelationDomain); + expect(domain.id).toBe(1); + expect(domain.positionId).toBe(11); + expect(domain.competenceId).toBe(7); + expect(domain.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/infrastructure/mappers/position-question-template-relation.mapper.spec.ts b/apps/api/test/unit/contexts/library/infrastructure/mappers/position-question-template-relation.mapper.spec.ts new file mode 100644 index 00000000..8d39e6fa --- /dev/null +++ b/apps/api/test/unit/contexts/library/infrastructure/mappers/position-question-template-relation.mapper.spec.ts @@ -0,0 +1,29 @@ +import { PositionQuestionTemplateRelation as PrismaPositionQuestionTemplateRelation } from '@intra/database'; +import { PositionQuestionTemplateRelationDomain } from 'src/contexts/library/domain/position-question-template-relation.domain'; +import { PositionQuestionTemplateRelationMapper } from 'src/contexts/library/infrastructure/mappers/position-question-template-relation.mapper'; + +describe('PositionQuestionTemplateRelationMapper', () => { + const prismaRow = { + id: 1, + questionTemplateId: 50, + positionId: 11, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaPositionQuestionTemplateRelation; + + describe('toDomain', () => { + it('converts a prisma row into a PositionQuestionTemplateRelationDomain', () => { + const domain = + PositionQuestionTemplateRelationMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf( + PositionQuestionTemplateRelationDomain, + ); + expect(domain.id).toBe(1); + expect(domain.questionTemplateId).toBe(50); + expect(domain.positionId).toBe(11); + expect(domain.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/infrastructure/mappers/question-template.mapper.spec.ts b/apps/api/test/unit/contexts/library/infrastructure/mappers/question-template.mapper.spec.ts new file mode 100644 index 00000000..33f0731e --- /dev/null +++ b/apps/api/test/unit/contexts/library/infrastructure/mappers/question-template.mapper.spec.ts @@ -0,0 +1,100 @@ +import { QuestionTemplate as PrismaQuestionTemplate } from '@intra/database'; +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { QuestionTemplateMapper } from 'src/contexts/library/infrastructure/mappers/question-template.mapper'; + +describe('QuestionTemplateMapper', () => { + const prismaTemplate = { + id: 1, + title: 'How is teamwork?', + answerType: 'NUMERICAL_SCALE' as PrismaQuestionTemplate['answerType'], + competenceId: 7, + isForSelfassessment: true, + status: 'ACTIVE' as PrismaQuestionTemplate['status'], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaQuestionTemplate; + + describe('toDomain', () => { + it('converts a prisma row into a QuestionTemplateDomain', () => { + const domain = QuestionTemplateMapper.toDomain(prismaTemplate); + + expect(domain).toBeInstanceOf(QuestionTemplateDomain); + expect(domain.id).toBe(1); + expect(domain.title).toBe('How is teamwork?'); + expect(domain.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(domain.competenceId).toBe(7); + expect(domain.isForSelfassessment).toBe(true); + expect(domain.status).toBe(QuestionTemplateStatus.ACTIVE); + expect(domain.positionIds).toEqual([]); + }); + + it('coerces null isForSelfassessment to false', () => { + const domain = QuestionTemplateMapper.toDomain({ + ...prismaTemplate, + isForSelfassessment: null, + } as unknown as PrismaQuestionTemplate); + + expect(domain.isForSelfassessment).toBe(false); + }); + + it('reads positionIds from positionRelations when supplied', () => { + const domain = QuestionTemplateMapper.toDomain({ + ...prismaTemplate, + positionRelations: [{ positionId: 3 }, { positionId: 4 }], + } as any); + + expect(domain.positionIds).toEqual([3, 4]); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input', () => { + const domain = QuestionTemplateDomain.create({ + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + competenceId: 7, + isForSelfassessment: false, + status: QuestionTemplateStatus.ARCHIVE, + }); + + const prisma = QuestionTemplateMapper.toPrisma(domain); + + expect(prisma).toEqual({ + title: 'Q?', + answerType: AnswerType.TEXT_FIELD, + competenceId: 7, + isForSelfassessment: false, + status: QuestionTemplateStatus.ARCHIVE, + }); + }); + }); + + describe('enum conversions', () => { + it('uppercases the answer type in both directions', () => { + expect( + QuestionTemplateMapper.toPrismaAnswerType( + AnswerType.NUMERICAL_SCALE, + ), + ).toBe('NUMERICAL_SCALE'); + expect( + QuestionTemplateMapper.fromPrismaAnswerType( + 'TEXT_FIELD' as PrismaQuestionTemplate['answerType'], + ), + ).toBe(AnswerType.TEXT_FIELD); + }); + + it('uppercases the status in both directions', () => { + expect( + QuestionTemplateMapper.toPrismaStatus( + QuestionTemplateStatus.ARCHIVE, + ), + ).toBe('ARCHIVE'); + expect( + QuestionTemplateMapper.fromPrismaStatus( + 'ACTIVE' as PrismaQuestionTemplate['status'], + ), + ).toBe(QuestionTemplateStatus.ACTIVE); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/presentation/controllers/clusters.controller.spec.ts b/apps/api/test/unit/contexts/library/presentation/controllers/clusters.controller.spec.ts new file mode 100644 index 00000000..e468c34e --- /dev/null +++ b/apps/api/test/unit/contexts/library/presentation/controllers/clusters.controller.spec.ts @@ -0,0 +1,103 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { ClusterService } from 'src/contexts/library/application/services/cluster.service'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { ClustersController } from 'src/contexts/library/presentation/http/controllers/clusters.controller'; + +function buildCluster(): ClusterDomain { + return ClusterDomain.create({ + id: 1, + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'Top performer', + description: 'Above 4.5', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +describe('ClustersController', () => { + let controller: ClustersController; + let service: any; + + beforeEach(() => { + service = { + create: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }; + + controller = new ClustersController( + service as unknown as ClusterService, + ); + }); + + describe('create', () => { + it('forwards the dto and maps the response', async () => { + service.create.mockResolvedValue(buildCluster()); + + const result = await controller.create({ + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'Top performer', + description: 'Above 4.5', + } as any); + + expect(service.create).toHaveBeenCalledWith({ + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'Top performer', + description: 'Above 4.5', + }); + expect(result.id).toBe(1); + expect(result.title).toBe('Top performer'); + }); + }); + + describe('search', () => { + it('maps every cluster to a response', async () => { + service.search.mockResolvedValue([buildCluster()]); + const result = await controller.search({} as any); + + expect(service.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('parses the id and forwards to the service', async () => { + service.getById.mockResolvedValue(buildCluster()); + + const result = await controller.getById('1'); + + expect(service.getById).toHaveBeenCalledWith(1); + expect(result.id).toBe(1); + }); + }); + + describe('update', () => { + it('parses the id and forwards the dto', async () => { + service.update.mockResolvedValue(buildCluster()); + + const result = await controller.update('1', { title: 'X' } as any); + + expect(service.update).toHaveBeenCalledWith(1, { title: 'X' }); + expect(result.title).toBe('Top performer'); + }); + }); + + describe('delete', () => { + it('parses the id and forwards to the service', async () => { + await controller.delete('1'); + expect(service.delete).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/presentation/controllers/competences.controller.spec.ts b/apps/api/test/unit/contexts/library/presentation/controllers/competences.controller.spec.ts new file mode 100644 index 00000000..ec53f02a --- /dev/null +++ b/apps/api/test/unit/contexts/library/presentation/controllers/competences.controller.spec.ts @@ -0,0 +1,150 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { CompetenceService } from 'src/contexts/library/application/services/competence.service'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetencesController } from 'src/contexts/library/presentation/http/controllers/competences.controller'; + +function buildCompetence(): CompetenceDomain { + return CompetenceDomain.create({ + id: 1, + code: 'TWK', + title: 'Teamwork', + description: 'desc', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +describe('CompetencesController', () => { + let controller: CompetencesController; + let service: any; + + beforeEach(() => { + service = { + create: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + attachPosition: jest.fn(), + detachPosition: jest.fn(), + listPositions: jest.fn(), + attachQuestionTemplate: jest.fn(), + detachQuestionTemplate: jest.fn(), + listQuestionTemplates: jest.fn(), + }; + + controller = new CompetencesController( + service as unknown as CompetenceService, + ); + }); + + describe('create', () => { + it('forwards the dto and maps the response', async () => { + service.create.mockResolvedValue(buildCompetence()); + + const result = await controller.create({ + code: 'TWK', + title: 'Teamwork', + description: 'desc', + } as any); + + expect(service.create).toHaveBeenCalledWith({ + code: 'TWK', + title: 'Teamwork', + description: 'desc', + }); + expect(result.id).toBe(1); + }); + }); + + describe('search', () => { + it('maps every competence to a response', async () => { + service.search.mockResolvedValue([buildCompetence()]); + const result = await controller.search({} as any); + expect(service.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('parses the id and forwards to the service', async () => { + service.getById.mockResolvedValue(buildCompetence()); + const result = await controller.getById('1'); + expect(service.getById).toHaveBeenCalledWith(1); + expect(result.id).toBe(1); + }); + }); + + describe('update', () => { + it('parses the id and forwards the dto', async () => { + service.update.mockResolvedValue(buildCompetence()); + await controller.update('1', { title: 'X' } as any); + expect(service.update).toHaveBeenCalledWith(1, { title: 'X' }); + }); + }); + + describe('delete', () => { + it('parses the id and forwards to the service', async () => { + await controller.delete('1'); + expect(service.delete).toHaveBeenCalledWith(1); + }); + }); + + describe('attachPosition', () => { + it('parses the path id and forwards the dto', async () => { + service.attachPosition.mockResolvedValue([11]); + const result = await controller.attachPosition('1', { + positionId: 11, + } as any); + expect(service.attachPosition).toHaveBeenCalledWith(1, 11); + expect(result).toEqual([11]); + }); + }); + + describe('detachPosition', () => { + it('parses both ids and forwards the call', async () => { + await controller.detachPosition('1', '11'); + expect(service.detachPosition).toHaveBeenCalledWith(1, 11); + }); + }); + + describe('listPositions', () => { + it('forwards the call and returns the result', async () => { + service.listPositions.mockResolvedValue([1, 2]); + const result = await controller.listPositions('1'); + expect(service.listPositions).toHaveBeenCalledWith(1); + expect(result).toEqual([1, 2]); + }); + }); + + describe('attachQuestionTemplate', () => { + it('parses the path id and forwards the dto', async () => { + service.attachQuestionTemplate.mockResolvedValue([50]); + const result = await controller.attachQuestionTemplate('1', { + questionTemplateId: 50, + } as any); + expect(service.attachQuestionTemplate).toHaveBeenCalledWith(1, 50); + expect(result).toEqual([50]); + }); + }); + + describe('detachQuestionTemplate', () => { + it('parses both ids and forwards the call', async () => { + await controller.detachQuestionTemplate('1', '50'); + expect(service.detachQuestionTemplate).toHaveBeenCalledWith(1, 50); + }); + }); + + describe('listQuestionTemplates', () => { + it('forwards the call and returns the result', async () => { + service.listQuestionTemplates.mockResolvedValue([50]); + const result = await controller.listQuestionTemplates('1'); + expect(service.listQuestionTemplates).toHaveBeenCalledWith(1); + expect(result).toEqual([50]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/presentation/controllers/question-templates.controller.spec.ts b/apps/api/test/unit/contexts/library/presentation/controllers/question-templates.controller.spec.ts new file mode 100644 index 00000000..96f704b2 --- /dev/null +++ b/apps/api/test/unit/contexts/library/presentation/controllers/question-templates.controller.spec.ts @@ -0,0 +1,158 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { QuestionTemplatesController } from 'src/contexts/library/presentation/http/controllers/question-templates.controller'; + +function buildTemplate(): QuestionTemplateDomain { + return QuestionTemplateDomain.create({ + id: 1, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 7, + status: QuestionTemplateStatus.ACTIVE, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +describe('QuestionTemplatesController', () => { + let controller: QuestionTemplatesController; + let service: any; + + beforeEach(() => { + service = { + create: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + attachPosition: jest.fn(), + detachPosition: jest.fn(), + listPositions: jest.fn(), + attachCompetence: jest.fn(), + detachCompetence: jest.fn(), + listCompetences: jest.fn(), + }; + + controller = new QuestionTemplatesController( + service as unknown as QuestionTemplateService, + ); + }); + + describe('create', () => { + it('forwards the dto and maps the response', async () => { + service.create.mockResolvedValue(buildTemplate()); + + const result = await controller.create({ + competenceId: 7, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + isForSelfassessment: false, + status: QuestionTemplateStatus.ACTIVE, + positionIds: [1, 2], + } as any); + + expect(service.create).toHaveBeenCalledWith({ + competenceId: 7, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + isForSelfassessment: false, + status: QuestionTemplateStatus.ACTIVE, + positionIds: [1, 2], + }); + expect(result.id).toBe(1); + }); + }); + + describe('search', () => { + it('maps every template to a response', async () => { + service.search.mockResolvedValue([buildTemplate()]); + const result = await controller.search({} as any); + expect(service.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('parses the id and forwards to the service', async () => { + service.getById.mockResolvedValue(buildTemplate()); + const result = await controller.getById('1'); + expect(service.getById).toHaveBeenCalledWith(1); + expect(result.id).toBe(1); + }); + }); + + describe('update', () => { + it('parses the id and forwards the dto', async () => { + service.update.mockResolvedValue(buildTemplate()); + await controller.update('1', { title: 'X' } as any); + expect(service.update).toHaveBeenCalledWith(1, { title: 'X' }); + }); + }); + + describe('delete', () => { + it('parses the id and forwards to the service', async () => { + await controller.delete('1'); + expect(service.delete).toHaveBeenCalledWith(1); + }); + }); + + describe('attachPosition', () => { + it('parses the path id and forwards the dto', async () => { + service.attachPosition.mockResolvedValue([11]); + const result = await controller.attachPosition('1', { + positionId: 11, + } as any); + expect(service.attachPosition).toHaveBeenCalledWith(1, 11); + expect(result).toEqual([11]); + }); + }); + + describe('detachPosition', () => { + it('parses both ids and forwards the call', async () => { + await controller.detachPosition('1', '11'); + expect(service.detachPosition).toHaveBeenCalledWith(1, 11); + }); + }); + + describe('listPositions', () => { + it('forwards the call and returns the result', async () => { + service.listPositions.mockResolvedValue([1, 2]); + const result = await controller.listPositions('1'); + expect(service.listPositions).toHaveBeenCalledWith(1); + expect(result).toEqual([1, 2]); + }); + }); + + describe('attachCompetence', () => { + it('parses the path id and forwards the dto', async () => { + service.attachCompetence.mockResolvedValue([7]); + const result = await controller.attachCompetence('1', { + competenceId: 7, + } as any); + expect(service.attachCompetence).toHaveBeenCalledWith(1, 7); + expect(result).toEqual([7]); + }); + }); + + describe('detachCompetence', () => { + it('parses both ids and forwards the call', async () => { + await controller.detachCompetence('1', '7'); + expect(service.detachCompetence).toHaveBeenCalledWith(1, 7); + }); + }); + + describe('listCompetences', () => { + it('forwards the call and returns the result', async () => { + service.listCompetences.mockResolvedValue([7]); + const result = await controller.listCompetences('1'); + expect(service.listCompetences).toHaveBeenCalledWith(1); + expect(result).toEqual([7]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/presentation/mappers/cluster.http.mapper.spec.ts b/apps/api/test/unit/contexts/library/presentation/mappers/cluster.http.mapper.spec.ts new file mode 100644 index 00000000..3d6a1682 --- /dev/null +++ b/apps/api/test/unit/contexts/library/presentation/mappers/cluster.http.mapper.spec.ts @@ -0,0 +1,34 @@ +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { ClusterHttpMapper } from 'src/contexts/library/presentation/http/mappers/cluster.http.mapper'; + +describe('ClusterHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = ClusterDomain.create({ + id: 1, + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'Top performer', + description: 'Above 4.5', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = ClusterHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.competenceId).toBe(7); + expect(response.lowerBound).toBe(0); + expect(response.upperBound).toBe(5); + expect(response.title).toBe('Top performer'); + expect(response.description).toBe('Above 4.5'); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/presentation/mappers/competence.http.mapper.spec.ts b/apps/api/test/unit/contexts/library/presentation/mappers/competence.http.mapper.spec.ts new file mode 100644 index 00000000..e426f4b5 --- /dev/null +++ b/apps/api/test/unit/contexts/library/presentation/mappers/competence.http.mapper.spec.ts @@ -0,0 +1,42 @@ +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { CompetenceHttpMapper } from 'src/contexts/library/presentation/http/mappers/competence.http.mapper'; + +describe('CompetenceHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = CompetenceDomain.create({ + id: 1, + code: 'TWK', + title: 'Teamwork', + description: 'Working with peers', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = CompetenceHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.code).toBe('TWK'); + expect(response.title).toBe('Teamwork'); + expect(response.description).toBe('Working with peers'); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('passes null code/description through to the response', () => { + const domain = CompetenceDomain.create({ + id: 1, + title: 'Teamwork', + }); + + const response = CompetenceHttpMapper.toResponse(domain); + + expect(response.code).toBeNull(); + expect(response.description).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/library/presentation/mappers/question-template.http.mapper.spec.ts b/apps/api/test/unit/contexts/library/presentation/mappers/question-template.http.mapper.spec.ts new file mode 100644 index 00000000..b80019b8 --- /dev/null +++ b/apps/api/test/unit/contexts/library/presentation/mappers/question-template.http.mapper.spec.ts @@ -0,0 +1,37 @@ +import { AnswerType, QuestionTemplateStatus } from '@intra/shared-kernel'; +import { QuestionTemplateDomain } from 'src/contexts/library/domain/question-template.domain'; +import { QuestionTemplateHttpMapper } from 'src/contexts/library/presentation/http/mappers/question-template.http.mapper'; + +describe('QuestionTemplateHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = QuestionTemplateDomain.create({ + id: 1, + competenceId: 7, + title: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + isForSelfassessment: true, + status: QuestionTemplateStatus.ACTIVE, + positionIds: [10, 11], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = QuestionTemplateHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.competenceId).toBe(7); + expect(response.title).toBe('Q?'); + expect(response.answerType).toBe(AnswerType.NUMERICAL_SCALE); + expect(response.isForSelfassessment).toBe(true); + expect(response.status).toBe(QuestionTemplateStatus.ACTIVE); + expect(response.positionIds).toEqual([10, 11]); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/application/listeners/cycle-stage-notification.listener.spec.ts b/apps/api/test/unit/contexts/notifications/application/listeners/cycle-stage-notification.listener.spec.ts new file mode 100644 index 00000000..26356699 --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/application/listeners/cycle-stage-notification.listener.spec.ts @@ -0,0 +1,78 @@ +import { CycleStage } from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { CycleStageChangedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-changed.event'; +import { CycleStageNotificationListener } from 'src/contexts/notifications/application/listeners/cycle-stage-notification.listener'; +import { ReviewEmailNotificationService } from 'src/contexts/notifications/application/services/review-email-notification.service'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('CycleStageNotificationListener', () => { + let listener: CycleStageNotificationListener; + let notifications: any; + + beforeEach(async () => { + notifications = { notifyCycleStrategicReportReady: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + CycleStageNotificationListener, + { + provide: ReviewEmailNotificationService, + useValue: notifications, + }, + ], + }).compile(); + + listener = module.get(CycleStageNotificationListener); + }); + + it('triggers the strategic-report email when the cycle reaches PUBLISHED', async () => { + notifications.notifyCycleStrategicReportReady.mockResolvedValue(1); + + const event = new CycleStageChangedEvent( + 100, + CycleStage.PREPARING_REPORT, + CycleStage.PUBLISHED, + ); + + await listener.handle(event); + + expect( + notifications.notifyCycleStrategicReportReady, + ).toHaveBeenCalledWith(100); + }); + + it('ignores other cycle stage transitions', async () => { + await listener.handle( + new CycleStageChangedEvent(100, CycleStage.NEW, CycleStage.ACTIVE), + ); + + expect( + notifications.notifyCycleStrategicReportReady, + ).not.toHaveBeenCalled(); + }); + + it('swallows errors thrown by the notification service', async () => { + notifications.notifyCycleStrategicReportReady.mockRejectedValue( + new Error('boom'), + ); + + await expect( + listener.handle( + new CycleStageChangedEvent( + 100, + CycleStage.PREPARING_REPORT, + CycleStage.PUBLISHED, + ), + ), + ).resolves.toBeUndefined(); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/application/listeners/review-stage-notification.listener.spec.ts b/apps/api/test/unit/contexts/notifications/application/listeners/review-stage-notification.listener.spec.ts new file mode 100644 index 00000000..061ebce7 --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/application/listeners/review-stage-notification.listener.spec.ts @@ -0,0 +1,97 @@ +import { ReviewStage } from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ReviewStageChangedEvent } from 'src/contexts/feedback360/application/events/review-stage-changed.event'; +import { ReviewStageNotificationListener } from 'src/contexts/notifications/application/listeners/review-stage-notification.listener'; +import { ReviewEmailNotificationService } from 'src/contexts/notifications/application/services/review-email-notification.service'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('ReviewStageNotificationListener', () => { + let listener: ReviewStageNotificationListener; + let notifications: any; + + beforeEach(async () => { + notifications = { + notifyRateeSelfAssessment: jest.fn(), + notifyRespondents: jest.fn(), + notifyHrReportReady: jest.fn(), + notifyReviewers: jest.fn(), + }; + + const module = await Test.createTestingModule({ + providers: [ + ReviewStageNotificationListener, + { + provide: ReviewEmailNotificationService, + useValue: notifications, + }, + ], + }).compile(); + + listener = module.get(ReviewStageNotificationListener); + }); + + function event(toStage: ReviewStage): ReviewStageChangedEvent { + return new ReviewStageChangedEvent(10, ReviewStage.NEW, toStage); + } + + it('triggers the ratee self-assessment email when the review reaches SELF_ASSESSMENT', async () => { + notifications.notifyRateeSelfAssessment.mockResolvedValue(1); + + await listener.handle(event(ReviewStage.SELF_ASSESSMENT)); + + expect(notifications.notifyRateeSelfAssessment).toHaveBeenCalledWith( + 10, + ); + expect(notifications.notifyRespondents).not.toHaveBeenCalled(); + }); + + it('triggers the respondents email when the review reaches IN_PROGRESS', async () => { + notifications.notifyRespondents.mockResolvedValue(3); + + await listener.handle(event(ReviewStage.IN_PROGRESS)); + + expect(notifications.notifyRespondents).toHaveBeenCalledWith(10); + }); + + it('triggers the HR report-ready email when the review reaches PROCESSING_BY_HR', async () => { + notifications.notifyHrReportReady.mockResolvedValue(1); + + await listener.handle(event(ReviewStage.PROCESSING_BY_HR)); + + expect(notifications.notifyHrReportReady).toHaveBeenCalledWith(10); + }); + + it('triggers the reviewers email when the review reaches PUBLISHED', async () => { + notifications.notifyReviewers.mockResolvedValue(2); + + await listener.handle(event(ReviewStage.PUBLISHED)); + + expect(notifications.notifyReviewers).toHaveBeenCalledWith(10); + }); + + it('ignores other review stage transitions', async () => { + await listener.handle(event(ReviewStage.FINISHED)); + + expect(notifications.notifyRateeSelfAssessment).not.toHaveBeenCalled(); + expect(notifications.notifyRespondents).not.toHaveBeenCalled(); + expect(notifications.notifyHrReportReady).not.toHaveBeenCalled(); + expect(notifications.notifyReviewers).not.toHaveBeenCalled(); + }); + + it('swallows errors thrown by the notification service', async () => { + notifications.notifyRespondents.mockRejectedValue(new Error('boom')); + + await expect( + listener.handle(event(ReviewStage.IN_PROGRESS)), + ).resolves.toBeUndefined(); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/application/listeners/user-created-notification.listener.spec.ts b/apps/api/test/unit/contexts/notifications/application/listeners/user-created-notification.listener.spec.ts new file mode 100644 index 00000000..ecc56b2f --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/application/listeners/user-created-notification.listener.spec.ts @@ -0,0 +1,51 @@ +import { Logger } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { UserCreatedEvent } from 'src/contexts/identity/application/events/user-created.event'; +import { UserCreatedNotificationListener } from 'src/contexts/notifications/application/listeners/user-created-notification.listener'; +import { ReviewEmailNotificationService } from 'src/contexts/notifications/application/services/review-email-notification.service'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('UserCreatedNotificationListener', () => { + let listener: UserCreatedNotificationListener; + let notifications: any; + + beforeEach(async () => { + notifications = { notifyUserWelcome: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + UserCreatedNotificationListener, + { + provide: ReviewEmailNotificationService, + useValue: notifications, + }, + ], + }).compile(); + + listener = module.get(UserCreatedNotificationListener); + }); + + it('forwards user.created events to the welcome notification', async () => { + notifications.notifyUserWelcome.mockResolvedValue(1); + + await listener.handle(new UserCreatedEvent(42, 'jane@example.com')); + + expect(notifications.notifyUserWelcome).toHaveBeenCalledWith(42); + }); + + it('swallows errors thrown by the notification service', async () => { + notifications.notifyUserWelcome.mockRejectedValue(new Error('boom')); + + await expect( + listener.handle(new UserCreatedEvent(42, 'jane@example.com')), + ).resolves.toBeUndefined(); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/application/services/review-email-notification.service.spec.ts b/apps/api/test/unit/contexts/notifications/application/services/review-email-notification.service.spec.ts new file mode 100644 index 00000000..7a9a95fd --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/application/services/review-email-notification.service.spec.ts @@ -0,0 +1,498 @@ +import { + IdentityRole, + IdentityStatus, + RespondentCategory, +} from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { RESPONDENT_REPOSITORY } from 'src/contexts/feedback360/application/ports/respondent.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { REVIEWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/reviewer.repository.port'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { MAILER } from 'src/contexts/notifications/application/ports/mailer.port'; +import { NOTIFICATION_LOG_REPOSITORY } from 'src/contexts/notifications/application/ports/notification-log.repository.port'; +import { ReviewEmailNotificationService } from 'src/contexts/notifications/application/services/review-email-notification.service'; +import { NotificationKind } from 'src/contexts/notifications/domain/notification-kind.enum'; +import { NotificationLogDomain } from 'src/contexts/notifications/domain/notification-log.domain'; +import { STRATEGIC_REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report.repository.port'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +function buildUser( + overrides: Partial[0]> = {}, +): UserDomain { + return UserDomain.create({ + id: 200, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + roles: [IdentityRole.EMPLOYEE], + ...overrides, + }); +} + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 10, + rateeId: 200, + rateeFullName: 'Jane Doe', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 7, + hrFullName: 'HR Admin', + reportId: 99, + ...overrides, + }); +} + +function buildLog( + overrides: Partial[0]> = {}, +): NotificationLogDomain { + return NotificationLogDomain.create({ + id: 1, + kind: NotificationKind.RATEE_SELF_ASSESSMENT, + recipientUserId: 200, + recipientEmail: 'jane@example.com', + ...overrides, + }); +} + +describe('ReviewEmailNotificationService', () => { + let service: ReviewEmailNotificationService; + let mailer: { sendMail: jest.Mock }; + let notificationLogRepo: any; + let reviewRepo: any; + let respondentRepo: any; + let reviewerRepo: any; + let userRepo: any; + let strategicReportRepo: any; + let cycleService: any; + let configService: any; + + beforeEach(async () => { + mailer = { sendMail: jest.fn() }; + notificationLogRepo = { + findOneForReview: jest.fn(), + findOneForCycle: jest.fn(), + findOneForUser: jest.fn(), + create: jest.fn(), + markSent: jest.fn(), + markFailure: jest.fn(), + }; + reviewRepo = { findById: jest.fn() }; + respondentRepo = { listByReview: jest.fn() }; + reviewerRepo = { listByReview: jest.fn() }; + userRepo = { findById: jest.fn() }; + strategicReportRepo = { findByCycleId: jest.fn() }; + cycleService = { getById: jest.fn() }; + configService = { + get: jest.fn((key: string) => { + if (key === 'app.frontendLink') return 'https://intra.test/'; + if (key === 'app.supportEmail') return 'support@intra.test'; + return undefined; + }), + }; + + const module = await Test.createTestingModule({ + providers: [ + ReviewEmailNotificationService, + { provide: MAILER, useValue: mailer }, + { + provide: NOTIFICATION_LOG_REPOSITORY, + useValue: notificationLogRepo, + }, + { provide: REVIEW_REPOSITORY, useValue: reviewRepo }, + { provide: RESPONDENT_REPOSITORY, useValue: respondentRepo }, + { provide: REVIEWER_REPOSITORY, useValue: reviewerRepo }, + { provide: IDENTITY_USER_REPOSITORY, useValue: userRepo }, + { + provide: STRATEGIC_REPORT_REPOSITORY, + useValue: strategicReportRepo, + }, + { provide: CycleService, useValue: cycleService }, + { provide: ConfigService, useValue: configService }, + ], + }).compile(); + + service = module.get(ReviewEmailNotificationService); + }); + + describe('notifyUserWelcome', () => { + it('returns 0 when the user does not exist', async () => { + userRepo.findById.mockResolvedValue(null); + + const sent = await service.notifyUserWelcome(200); + + expect(sent).toBe(0); + expect(mailer.sendMail).not.toHaveBeenCalled(); + }); + + it('creates a per-user log, sends the email, and marks it sent', async () => { + userRepo.findById.mockResolvedValue(buildUser()); + notificationLogRepo.findOneForUser.mockResolvedValue(null); + notificationLogRepo.create.mockImplementation( + async (log: NotificationLogDomain) => + NotificationLogDomain.create({ ...log, id: 42 }), + ); + + const sent = await service.notifyUserWelcome(200); + + expect(sent).toBe(1); + expect(notificationLogRepo.findOneForUser).toHaveBeenCalledWith( + NotificationKind.USER_WELCOME, + 200, + ); + expect(notificationLogRepo.create).toHaveBeenCalledTimes(1); + const created = notificationLogRepo.create.mock.calls[0][0]; + expect(created.reviewId).toBeNull(); + expect(created.cycleId).toBeNull(); + expect(created.kind).toBe(NotificationKind.USER_WELCOME); + + expect(mailer.sendMail).toHaveBeenCalledTimes(1); + const mailCall = mailer.sendMail.mock.calls[0][0]; + expect(mailCall.to).toBe('jane@example.com'); + expect(mailCall.template).toBe('user-welcome'); + expect(mailCall.context.userFirstName).toBe('Jane'); + expect(mailCall.context.actionUrl).toBe( + 'https://intra.test/dashboard', + ); + expect(mailCall.context.supportEmail).toBe('support@intra.test'); + expect(mailCall.attachments).toHaveLength(1); + expect(notificationLogRepo.markSent).toHaveBeenCalledWith(42); + }); + + it('skips dispatch when a sent log already exists for the user', async () => { + userRepo.findById.mockResolvedValue(buildUser()); + notificationLogRepo.findOneForUser.mockResolvedValue( + buildLog({ + kind: NotificationKind.USER_WELCOME, + sentAt: new Date('2025-01-01T00:00:00.000Z'), + }), + ); + + const sent = await service.notifyUserWelcome(200); + + expect(sent).toBe(0); + expect(notificationLogRepo.create).not.toHaveBeenCalled(); + expect(mailer.sendMail).not.toHaveBeenCalled(); + }); + + it('reuses an existing unsent log without creating a new one', async () => { + userRepo.findById.mockResolvedValue(buildUser()); + const existing = buildLog({ + id: 7, + kind: NotificationKind.USER_WELCOME, + }); + notificationLogRepo.findOneForUser.mockResolvedValue(existing); + + const sent = await service.notifyUserWelcome(200); + + expect(sent).toBe(1); + expect(notificationLogRepo.create).not.toHaveBeenCalled(); + expect(notificationLogRepo.markSent).toHaveBeenCalledWith(7); + }); + + it('marks failure when mailer.sendMail throws', async () => { + userRepo.findById.mockResolvedValue(buildUser()); + notificationLogRepo.findOneForUser.mockResolvedValue(null); + notificationLogRepo.create.mockResolvedValue(buildLog({ id: 42 })); + mailer.sendMail.mockRejectedValue(new Error('SMTP exploded')); + + const sent = await service.notifyUserWelcome(200); + + expect(sent).toBe(0); + expect(notificationLogRepo.markFailure).toHaveBeenCalledWith( + 42, + 'SMTP exploded', + ); + expect(notificationLogRepo.markSent).not.toHaveBeenCalled(); + }); + + it('skips dispatch when the recipient has no email', async () => { + userRepo.findById.mockResolvedValue( + UserDomain.create({ + id: 200, + firstName: 'Jane', + lastName: 'Doe', + email: '', + }), + ); + + const sent = await service.notifyUserWelcome(200); + + expect(sent).toBe(0); + expect(notificationLogRepo.findOneForUser).not.toHaveBeenCalled(); + }); + }); + + describe('notifyCycleStrategicReportReady', () => { + it('returns 0 when the HR user is not found', async () => { + cycleService.getById.mockResolvedValue({ + id: 100, + title: 'Q1', + hrId: 7, + }); + strategicReportRepo.findByCycleId.mockResolvedValue({ id: 99 }); + userRepo.findById.mockResolvedValue(null); + + const sent = await service.notifyCycleStrategicReportReady(100); + + expect(sent).toBe(0); + expect(mailer.sendMail).not.toHaveBeenCalled(); + }); + + it('sends the strategic-report email to HR with the correct action URL', async () => { + cycleService.getById.mockResolvedValue({ + id: 100, + title: 'Q1 2025', + hrId: 7, + }); + strategicReportRepo.findByCycleId.mockResolvedValue({ id: 99 }); + userRepo.findById.mockResolvedValue( + buildUser({ id: 7, firstName: 'HR', email: 'hr@intra.test' }), + ); + notificationLogRepo.findOneForCycle.mockResolvedValue(null); + notificationLogRepo.create.mockResolvedValue(buildLog({ id: 42 })); + + const sent = await service.notifyCycleStrategicReportReady(100); + + expect(sent).toBe(1); + expect(notificationLogRepo.findOneForCycle).toHaveBeenCalledWith( + 100, + NotificationKind.CYCLE_STRATEGIC_REPORT_READY, + 7, + ); + const created = notificationLogRepo.create.mock.calls[0][0]; + expect(created.cycleId).toBe(100); + expect(created.reviewId).toBeNull(); + + const mailCall = mailer.sendMail.mock.calls[0][0]; + expect(mailCall.subject).toBe('Strategic report ready: Q1 2025'); + expect(mailCall.template).toBe('cycle-strategic-report-ready'); + expect(mailCall.context.cycleTitle).toBe('Q1 2025'); + expect(mailCall.context.actionUrl).toBe( + 'https://intra.test/reporting/strategic-reports/99', + ); + expect(notificationLogRepo.markSent).toHaveBeenCalledWith(42); + }); + }); + + describe('notifyRateeSelfAssessment', () => { + it('returns 0 when the review does not exist', async () => { + reviewRepo.findById.mockResolvedValue(null); + + const sent = await service.notifyRateeSelfAssessment(10); + + expect(sent).toBe(0); + }); + + it('returns 0 when the ratee user does not exist', async () => { + reviewRepo.findById.mockResolvedValue(buildReview()); + userRepo.findById.mockResolvedValue(null); + + const sent = await service.notifyRateeSelfAssessment(10); + + expect(sent).toBe(0); + }); + + it('sends the self-assessment email scoped to the review', async () => { + reviewRepo.findById.mockResolvedValue(buildReview()); + userRepo.findById.mockResolvedValue(buildUser()); + notificationLogRepo.findOneForReview.mockResolvedValue(null); + notificationLogRepo.create.mockResolvedValue(buildLog({ id: 42 })); + + const sent = await service.notifyRateeSelfAssessment(10); + + expect(sent).toBe(1); + expect(notificationLogRepo.findOneForReview).toHaveBeenCalledWith( + 10, + NotificationKind.RATEE_SELF_ASSESSMENT, + 200, + ); + const created = notificationLogRepo.create.mock.calls[0][0]; + expect(created.reviewId).toBe(10); + expect(created.cycleId).toBeNull(); + const mailCall = mailer.sendMail.mock.calls[0][0]; + expect(mailCall.template).toBe('ratee-self-assessment'); + expect(mailCall.context.actionUrl).toBe( + 'https://intra.test/feedback360/surveys/10', + ); + }); + }); + + describe('notifyRespondents', () => { + it('returns 0 when the review does not exist', async () => { + reviewRepo.findById.mockResolvedValue(null); + + const sent = await service.notifyRespondents(10); + + expect(sent).toBe(0); + }); + + it('iterates respondents, skips missing users, and counts successful sends', async () => { + reviewRepo.findById.mockResolvedValue(buildReview()); + respondentRepo.listByReview.mockResolvedValue([ + RespondentDomain.create({ + reviewId: 10, + respondentId: 201, + fullName: 'A B', + category: RespondentCategory.TEAM, + positionId: 1, + positionTitle: 'Engineer', + }), + RespondentDomain.create({ + reviewId: 10, + respondentId: 202, + fullName: 'C D', + category: RespondentCategory.OTHER, + positionId: 1, + positionTitle: 'Engineer', + }), + ]); + // First respondent: user exists, sends successfully + // Second respondent: user missing → skipped + userRepo.findById.mockImplementation(async (id: number) => + id === 201 + ? buildUser({ + id: 201, + firstName: 'A', + email: 'a@intra.test', + }) + : null, + ); + notificationLogRepo.findOneForReview.mockResolvedValue(null); + notificationLogRepo.create.mockImplementation( + async (log: NotificationLogDomain) => + NotificationLogDomain.create({ ...log, id: 42 }), + ); + + const sent = await service.notifyRespondents(10); + + expect(sent).toBe(1); + expect(mailer.sendMail).toHaveBeenCalledTimes(1); + const mailCall = mailer.sendMail.mock.calls[0][0]; + expect(mailCall.template).toBe('respondent-invitation'); + expect(mailCall.context.respondentFirstName).toBe('A'); + expect(mailCall.context.rateeFullName).toBe('Jane Doe'); + expect(mailCall.context.rateeFirstName).toBe('Jane'); + }); + }); + + describe('notifyHrReportReady', () => { + it('returns 0 when review or HR is missing', async () => { + reviewRepo.findById.mockResolvedValue(null); + await expect(service.notifyHrReportReady(10)).resolves.toBe(0); + + reviewRepo.findById.mockResolvedValue(buildReview()); + userRepo.findById.mockResolvedValue(null); + await expect(service.notifyHrReportReady(10)).resolves.toBe(0); + }); + + it('sends the HR report-ready email with the comments URL', async () => { + reviewRepo.findById.mockResolvedValue(buildReview()); + userRepo.findById.mockResolvedValue( + buildUser({ id: 7, firstName: 'HR', email: 'hr@intra.test' }), + ); + notificationLogRepo.findOneForReview.mockResolvedValue(null); + notificationLogRepo.create.mockResolvedValue(buildLog({ id: 42 })); + + const sent = await service.notifyHrReportReady(10); + + expect(sent).toBe(1); + const mailCall = mailer.sendMail.mock.calls[0][0]; + expect(mailCall.template).toBe('hr-report-ready'); + expect(mailCall.context.actionUrl).toBe( + 'https://intra.test/reporting/individual-reports/99/comments', + ); + }); + }); + + describe('notifyReviewers', () => { + it('returns 0 when the review is missing', async () => { + reviewRepo.findById.mockResolvedValue(null); + await expect(service.notifyReviewers(10)).resolves.toBe(0); + }); + + it('iterates reviewers, sends a per-reviewer email, and counts successful sends', async () => { + reviewRepo.findById.mockResolvedValue(buildReview()); + reviewerRepo.listByReview.mockResolvedValue([ + ReviewerDomain.create({ + reviewId: 10, + reviewerId: 300, + fullName: 'Rev One', + positionId: 1, + positionTitle: 'Manager', + }), + ]); + userRepo.findById.mockResolvedValue( + buildUser({ + id: 300, + firstName: 'Rev', + email: 'rev@intra.test', + }), + ); + notificationLogRepo.findOneForReview.mockResolvedValue(null); + notificationLogRepo.create.mockResolvedValue(buildLog({ id: 42 })); + + const sent = await service.notifyReviewers(10); + + expect(sent).toBe(1); + const mailCall = mailer.sendMail.mock.calls[0][0]; + expect(mailCall.template).toBe('reviewer-report-ready'); + expect(mailCall.context.reviewerFirstName).toBe('Rev'); + expect(mailCall.context.actionUrl).toBe( + 'https://intra.test/reporting/individual-reports/99', + ); + }); + }); + + describe('URL building', () => { + it('strips a trailing slash from the frontend base and prepends one to the path', async () => { + configService.get = jest.fn((key: string) => { + if (key === 'app.frontendLink') return 'https://intra.test/'; // trailing slash + if (key === 'app.supportEmail') return 'support@intra.test'; + return undefined; + }); + + userRepo.findById.mockResolvedValue(buildUser()); + notificationLogRepo.findOneForUser.mockResolvedValue(null); + notificationLogRepo.create.mockResolvedValue(buildLog({ id: 42 })); + + await service.notifyUserWelcome(200); + + const mailCall = mailer.sendMail.mock.calls[0][0]; + expect(mailCall.context.actionUrl).toBe( + 'https://intra.test/dashboard', + ); + }); + + it('falls back to "/" when no frontend link is configured', async () => { + configService.get = jest.fn(() => undefined); + + userRepo.findById.mockResolvedValue(buildUser()); + notificationLogRepo.findOneForUser.mockResolvedValue(null); + notificationLogRepo.create.mockResolvedValue(buildLog({ id: 42 })); + + await service.notifyUserWelcome(200); + + const mailCall = mailer.sendMail.mock.calls[0][0]; + expect(mailCall.context.actionUrl).toBe('/dashboard'); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/domain/notification-channel.enum.spec.ts b/apps/api/test/unit/contexts/notifications/domain/notification-channel.enum.spec.ts new file mode 100644 index 00000000..5544553f --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/domain/notification-channel.enum.spec.ts @@ -0,0 +1,7 @@ +import { NotificationChannel } from 'src/contexts/notifications/domain/notification-channel.enum'; + +describe('NotificationChannel', () => { + it('exposes the EMAIL channel value', () => { + expect(NotificationChannel.EMAIL).toBe('EMAIL'); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/domain/notification-kind.enum.spec.ts b/apps/api/test/unit/contexts/notifications/domain/notification-kind.enum.spec.ts new file mode 100644 index 00000000..e5f27456 --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/domain/notification-kind.enum.spec.ts @@ -0,0 +1,20 @@ +import { NotificationKind } from 'src/contexts/notifications/domain/notification-kind.enum'; + +describe('NotificationKind', () => { + it('exposes the expected notification kinds with string values matching the keys', () => { + expect(NotificationKind.RATEE_SELF_ASSESSMENT).toBe( + 'RATEE_SELF_ASSESSMENT', + ); + expect(NotificationKind.RESPONDENT_INVITATION).toBe( + 'RESPONDENT_INVITATION', + ); + expect(NotificationKind.HR_REPORT_READY).toBe('HR_REPORT_READY'); + expect(NotificationKind.REVIEWER_REPORT_READY).toBe( + 'REVIEWER_REPORT_READY', + ); + expect(NotificationKind.CYCLE_STRATEGIC_REPORT_READY).toBe( + 'CYCLE_STRATEGIC_REPORT_READY', + ); + expect(NotificationKind.USER_WELCOME).toBe('USER_WELCOME'); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/domain/notification-log.domain.spec.ts b/apps/api/test/unit/contexts/notifications/domain/notification-log.domain.spec.ts new file mode 100644 index 00000000..fcae743a --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/domain/notification-log.domain.spec.ts @@ -0,0 +1,67 @@ +import { NotificationChannel } from 'src/contexts/notifications/domain/notification-channel.enum'; +import { NotificationKind } from 'src/contexts/notifications/domain/notification-kind.enum'; +import { NotificationLogDomain } from 'src/contexts/notifications/domain/notification-log.domain'; + +describe('NotificationLogDomain', () => { + const baseProps = { + kind: NotificationKind.RATEE_SELF_ASSESSMENT, + recipientUserId: 200, + recipientEmail: 'jane@example.com', + }; + + describe('create', () => { + it('creates a log with every supplied property', () => { + const log = NotificationLogDomain.create({ + id: 1, + reviewId: 10, + cycleId: 100, + kind: NotificationKind.HR_REPORT_READY, + channel: NotificationChannel.EMAIL, + recipientUserId: 200, + recipientEmail: 'jane@example.com', + sentAt: new Date('2025-01-02T00:00:00.000Z'), + errorMessage: 'boom', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(log.id).toBe(1); + expect(log.reviewId).toBe(10); + expect(log.cycleId).toBe(100); + expect(log.kind).toBe(NotificationKind.HR_REPORT_READY); + expect(log.channel).toBe(NotificationChannel.EMAIL); + expect(log.recipientUserId).toBe(200); + expect(log.recipientEmail).toBe('jane@example.com'); + expect(log.sentAt).toEqual(new Date('2025-01-02T00:00:00.000Z')); + expect(log.errorMessage).toBe('boom'); + }); + + it('defaults channel to EMAIL when not provided', () => { + const log = NotificationLogDomain.create(baseProps); + expect(log.channel).toBe(NotificationChannel.EMAIL); + }); + + it('normalises missing fields to null', () => { + const log = NotificationLogDomain.create(baseProps); + expect(log.reviewId).toBeNull(); + expect(log.cycleId).toBeNull(); + expect(log.sentAt).toBeNull(); + expect(log.errorMessage).toBeNull(); + }); + }); + + describe('isSent', () => { + it('returns false when sentAt is null', () => { + const log = NotificationLogDomain.create(baseProps); + expect(log.isSent()).toBe(false); + }); + + it('returns true when sentAt is a Date', () => { + const log = NotificationLogDomain.create({ + ...baseProps, + sentAt: new Date('2025-01-02T00:00:00.000Z'), + }); + expect(log.isSent()).toBe(true); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/infrastructure/mail/nest-mailer.adapter.spec.ts b/apps/api/test/unit/contexts/notifications/infrastructure/mail/nest-mailer.adapter.spec.ts new file mode 100644 index 00000000..7c92865c --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/infrastructure/mail/nest-mailer.adapter.spec.ts @@ -0,0 +1,62 @@ +import { MailerService } from '@nestjs-modules/mailer'; +import { NestMailerAdapter } from 'src/contexts/notifications/infrastructure/mail/nest-mailer.adapter'; + +describe('NestMailerAdapter', () => { + let adapter: NestMailerAdapter; + let mailerService: { sendMail: jest.Mock }; + + beforeEach(() => { + mailerService = { sendMail: jest.fn() }; + adapter = new NestMailerAdapter( + mailerService as unknown as MailerService, + ); + }); + + describe('sendMail', () => { + it('forwards the options to the underlying mailer service', async () => { + await adapter.sendMail({ + to: 'jane@example.com', + subject: 'Hello', + template: 'user-welcome', + context: { userFirstName: 'Jane' }, + }); + + expect(mailerService.sendMail).toHaveBeenCalledWith({ + to: 'jane@example.com', + subject: 'Hello', + template: 'user-welcome', + context: { userFirstName: 'Jane' }, + attachments: undefined, + }); + }); + + it('maps every attachment field through to nodemailer', async () => { + const content = Buffer.from('abc'); + + await adapter.sendMail({ + to: 'jane@example.com', + subject: 'Hello', + template: 'user-welcome', + context: {}, + attachments: [ + { + filename: 'logo.png', + content, + contentType: 'image/png', + cid: 'intra-logo', + }, + ], + }); + + const passed = mailerService.sendMail.mock.calls[0][0]; + expect(passed.attachments).toEqual([ + { + filename: 'logo.png', + content, + contentType: 'image/png', + cid: 'intra-logo', + }, + ]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/infrastructure/mappers/notification-log.mapper.spec.ts b/apps/api/test/unit/contexts/notifications/infrastructure/mappers/notification-log.mapper.spec.ts new file mode 100644 index 00000000..1043858f --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/infrastructure/mappers/notification-log.mapper.spec.ts @@ -0,0 +1,88 @@ +import { NotificationLog as PrismaNotificationLog } from '@intra/database'; +import { NotificationChannel } from 'src/contexts/notifications/domain/notification-channel.enum'; +import { NotificationKind } from 'src/contexts/notifications/domain/notification-kind.enum'; +import { NotificationLogDomain } from 'src/contexts/notifications/domain/notification-log.domain'; +import { NotificationLogMapper } from 'src/contexts/notifications/infrastructure/mappers/notification-log.mapper'; + +describe('NotificationLogMapper', () => { + const prismaRow = { + id: 1, + reviewId: 10, + cycleId: 100, + kind: 'HR_REPORT_READY' as PrismaNotificationLog['kind'], + channel: 'EMAIL' as PrismaNotificationLog['channel'], + recipientUserId: 200, + recipientEmail: 'jane@example.com', + sentAt: new Date('2025-01-02T00:00:00.000Z'), + errorMessage: null, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaNotificationLog; + + describe('toDomain', () => { + it('converts a prisma row into a NotificationLogDomain', () => { + const domain = NotificationLogMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(NotificationLogDomain); + expect(domain.id).toBe(1); + expect(domain.reviewId).toBe(10); + expect(domain.cycleId).toBe(100); + expect(domain.kind).toBe(NotificationKind.HR_REPORT_READY); + expect(domain.channel).toBe(NotificationChannel.EMAIL); + expect(domain.recipientUserId).toBe(200); + expect(domain.recipientEmail).toBe('jane@example.com'); + expect(domain.sentAt).toEqual(new Date('2025-01-02T00:00:00.000Z')); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain object into a Prisma create input', () => { + const domain = NotificationLogDomain.create({ + reviewId: 10, + cycleId: null, + kind: NotificationKind.RATEE_SELF_ASSESSMENT, + recipientUserId: 200, + recipientEmail: 'jane@example.com', + }); + + const prisma = NotificationLogMapper.toPrisma(domain); + + expect(prisma.reviewId).toBe(10); + expect(prisma.cycleId).toBeNull(); + expect(prisma.kind).toBe('RATEE_SELF_ASSESSMENT'); + expect(prisma.channel).toBe('EMAIL'); + expect(prisma.recipientUserId).toBe(200); + expect(prisma.recipientEmail).toBe('jane@example.com'); + expect(prisma.sentAt).toBeNull(); + expect(prisma.errorMessage).toBeNull(); + }); + }); + + describe('enum conversions', () => { + it('passes the notification kind through unchanged in both directions', () => { + expect( + NotificationLogMapper.toPrismaKind( + NotificationKind.USER_WELCOME, + ), + ).toBe('USER_WELCOME'); + expect( + NotificationLogMapper.fromPrismaKind( + 'CYCLE_STRATEGIC_REPORT_READY' as PrismaNotificationLog['kind'], + ), + ).toBe(NotificationKind.CYCLE_STRATEGIC_REPORT_READY); + }); + + it('passes the notification channel through unchanged in both directions', () => { + expect( + NotificationLogMapper.toPrismaChannel( + NotificationChannel.EMAIL, + ), + ).toBe('EMAIL'); + expect( + NotificationLogMapper.fromPrismaChannel( + 'EMAIL' as PrismaNotificationLog['channel'], + ), + ).toBe(NotificationChannel.EMAIL); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/notifications/infrastructure/templates/logo-data-uri.spec.ts b/apps/api/test/unit/contexts/notifications/infrastructure/templates/logo-data-uri.spec.ts new file mode 100644 index 00000000..5ccb56fb --- /dev/null +++ b/apps/api/test/unit/contexts/notifications/infrastructure/templates/logo-data-uri.spec.ts @@ -0,0 +1,31 @@ +import { + LOGO_BUFFER, + LOGO_CID, + LOGO_CONTENT_TYPE, + LOGO_DATA_URI, + LOGO_FILENAME, +} from 'src/contexts/notifications/infrastructure/templates/logo-data-uri'; + +describe('logo-data-uri', () => { + it('exposes the canonical CID and filename', () => { + expect(LOGO_CID).toBe('intra-logo'); + expect(LOGO_FILENAME).toBe('intra-logo.png'); + expect(LOGO_CONTENT_TYPE).toBe('image/png'); + }); + + it('encodes a PNG data URI matching the buffer', () => { + expect(LOGO_DATA_URI.startsWith('data:image/png;base64,')).toBe(true); + + const base64 = LOGO_DATA_URI.split(',')[1]; + const decoded = Buffer.from(base64, 'base64'); + expect(decoded.equals(LOGO_BUFFER)).toBe(true); + }); + + it('starts the decoded buffer with the PNG magic header', () => { + // \x89PNG\r\n\x1a\n + const header = Buffer.from([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, + ]); + expect(LOGO_BUFFER.slice(0, 8).equals(header)).toBe(true); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/application/services/position-hierarchy.service.spec.ts b/apps/api/test/unit/contexts/organisation/application/services/position-hierarchy.service.spec.ts new file mode 100644 index 00000000..108949c3 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/application/services/position-hierarchy.service.spec.ts @@ -0,0 +1,110 @@ +import { BadRequestException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ORGANISATION_POSITION_HIERARCHY_REPOSITORY } from 'src/contexts/organisation/application/ports/position-hierarchy.repository.port'; +import { PositionHierarchyService } from 'src/contexts/organisation/application/services/position-hierarchy.service'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PositionHierarchyDomain } from 'src/contexts/organisation/domain/position-hierarchy.domain'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; + +function buildPosition(id: number): PositionDomain { + return PositionDomain.create({ id, title: `Position ${id}` }); +} + +describe('PositionHierarchyService', () => { + let service: PositionHierarchyService; + let hierarchy: any; + let positions: any; + + beforeEach(async () => { + hierarchy = { + link: jest.fn(), + unlink: jest.fn(), + listSubordinates: jest.fn(), + listSuperiors: jest.fn(), + }; + positions = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + PositionHierarchyService, + { + provide: ORGANISATION_POSITION_HIERARCHY_REPOSITORY, + useValue: hierarchy, + }, + { provide: PositionService, useValue: positions }, + ], + }).compile(); + + service = module.get(PositionHierarchyService); + }); + + describe('link', () => { + it('rejects when superior and subordinate are the same position', async () => { + await expect(service.link(1, 1)).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(hierarchy.link).not.toHaveBeenCalled(); + }); + + it('verifies both positions and links the relation', async () => { + positions.getById.mockResolvedValue(buildPosition(1)); + hierarchy.link.mockResolvedValue( + PositionHierarchyDomain.create({ + superiorPositionId: 1, + subordinatePositionId: 2, + }), + ); + + await service.link(1, 2); + + expect(positions.getById).toHaveBeenNthCalledWith(1, 1); + expect(positions.getById).toHaveBeenNthCalledWith(2, 2); + expect(hierarchy.link).toHaveBeenCalledWith(1, 2); + }); + }); + + describe('unlink', () => { + it('verifies both positions and unlinks the relation', async () => { + positions.getById.mockResolvedValue(buildPosition(1)); + + await service.unlink(1, 2); + + expect(hierarchy.unlink).toHaveBeenCalledWith(1, 2); + }); + }); + + describe('listSubordinates', () => { + it('resolves each subordinate position from the relations', async () => { + positions.getById.mockImplementation(async (id: number) => + buildPosition(id), + ); + hierarchy.listSubordinates.mockResolvedValue([ + { subordinatePositionId: 2 }, + { subordinatePositionId: 3 }, + ]); + + const result = await service.listSubordinates(1); + + // First getById is the access check for the superior, then one per relation + expect(positions.getById).toHaveBeenCalledWith(1); + expect(result.map((p) => p.id)).toEqual([2, 3]); + }); + }); + + describe('listSuperiors', () => { + it('resolves each superior position from the relations', async () => { + positions.getById.mockImplementation(async (id: number) => + buildPosition(id), + ); + hierarchy.listSuperiors.mockResolvedValue([ + { superiorPositionId: 7 }, + { superiorPositionId: 8 }, + ]); + + const result = await service.listSuperiors(2); + + expect(positions.getById).toHaveBeenCalledWith(2); + expect(result.map((p) => p.id)).toEqual([7, 8]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/application/services/position.service.spec.ts b/apps/api/test/unit/contexts/organisation/application/services/position.service.spec.ts new file mode 100644 index 00000000..4cb7ff24 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/application/services/position.service.spec.ts @@ -0,0 +1,163 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { POSITION_COMPETENCE_RELATION_REPOSITORY } from 'src/contexts/library/application/ports/position-competence-relation.repository.port'; +import { ORGANISATION_POSITION_REPOSITORY } from 'src/contexts/organisation/application/ports/position.repository.port'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; + +function buildPosition( + overrides: Partial[0]> = {}, +): PositionDomain { + return PositionDomain.create({ + id: 1, + title: 'Engineer', + ...overrides, + }); +} + +describe('PositionService', () => { + let service: PositionService; + let positions: any; + let positionCompetenceRelations: any; + + beforeEach(async () => { + positions = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + }; + positionCompetenceRelations = { listByPosition: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + PositionService, + { + provide: ORGANISATION_POSITION_REPOSITORY, + useValue: positions, + }, + { + provide: POSITION_COMPETENCE_RELATION_REPOSITORY, + useValue: positionCompetenceRelations, + }, + ], + }).compile(); + + service = module.get(PositionService); + }); + + describe('create', () => { + it('persists the position with null fallback for description', async () => { + positions.create.mockImplementation(async (p: PositionDomain) => p); + + await service.create({ title: 'Engineer' }); + + const passed = positions.create.mock.calls[0][0] as PositionDomain; + expect(passed).toBeInstanceOf(PositionDomain); + expect(passed.title).toBe('Engineer'); + expect(passed.description).toBeNull(); + }); + + it('passes through optional description', async () => { + positions.create.mockImplementation(async (p: PositionDomain) => p); + + await service.create({ + title: 'Engineer', + description: 'desc', + }); + + const passed = positions.create.mock.calls[0][0] as PositionDomain; + expect(passed.description).toBe('desc'); + }); + }); + + describe('search', () => { + it('delegates to the repository', async () => { + positions.search.mockResolvedValue([buildPosition()]); + const result = await service.search({}); + expect(positions.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('returns the position when found', async () => { + const position = buildPosition(); + positions.findById.mockResolvedValue(position); + await expect(service.getById(1)).resolves.toBe(position); + }); + + it('throws NotFoundException when missing', async () => { + positions.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('throws NotFoundException when the position does not exist', async () => { + positions.findById.mockResolvedValue(null); + await expect( + service.update(1, { title: 'X' }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('forwards the patch to the repository', async () => { + positions.findById.mockResolvedValue(buildPosition()); + positions.updateById.mockResolvedValue(buildPosition()); + + await service.update(1, { title: 'New', description: 'X' }); + + expect(positions.updateById).toHaveBeenCalledWith(1, { + title: 'New', + description: 'X', + }); + }); + }); + + describe('delete', () => { + it('throws NotFoundException when the position does not exist', async () => { + positions.findById.mockResolvedValue(null); + await expect(service.delete(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + expect(positions.deleteById).not.toHaveBeenCalled(); + }); + + it('rejects when there are competences linked to the position', async () => { + positions.findById.mockResolvedValue(buildPosition()); + positionCompetenceRelations.listByPosition.mockResolvedValue([ + { competenceId: 7 }, + ]); + + await expect(service.delete(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(positions.deleteById).not.toHaveBeenCalled(); + }); + + it('deletes the position when no competences are linked', async () => { + positions.findById.mockResolvedValue(buildPosition()); + positionCompetenceRelations.listByPosition.mockResolvedValue([]); + + await service.delete(1); + + expect(positions.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('listCompetences', () => { + it('maps relations to competence ids', async () => { + positions.findById.mockResolvedValue(buildPosition()); + positionCompetenceRelations.listByPosition.mockResolvedValue([ + { competenceId: 7 }, + { competenceId: 8 }, + ]); + + const result = await service.listCompetences(1); + expect(result).toEqual([7, 8]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/application/services/team.service.spec.ts b/apps/api/test/unit/contexts/organisation/application/services/team.service.spec.ts new file mode 100644 index 00000000..29871e26 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/application/services/team.service.spec.ts @@ -0,0 +1,286 @@ +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { ORGANISATION_TEAM_REPOSITORY } from 'src/contexts/organisation/application/ports/team.repository.port'; +import { TeamService } from 'src/contexts/organisation/application/services/team.service'; +import { TeamMembershipDomain } from 'src/contexts/organisation/domain/team-membership.domain'; +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; + +function buildTeam( + overrides: Partial[0]> = {}, +): TeamDomain { + return TeamDomain.create({ + id: 1, + title: 'Platform', + ...overrides, + }); +} + +function buildUser(id = 200): UserDomain { + return UserDomain.create({ + id, + firstName: 'Jane', + lastName: 'Doe', + email: `user${id}@example.com`, + status: IdentityStatus.ACTIVE, + roles: [IdentityRole.EMPLOYEE], + }); +} + +function buildMembership( + overrides: Partial[0]> = {}, +): TeamMembershipDomain { + return TeamMembershipDomain.create({ + id: 1, + teamId: 1, + memberId: 200, + ...overrides, + }); +} + +describe('TeamService', () => { + let service: TeamService; + let teams: any; + let identityUsers: any; + + beforeEach(async () => { + teams = { + create: jest.fn(), + search: jest.fn(), + findById: jest.fn(), + updateById: jest.fn(), + deleteById: jest.fn(), + addMember: jest.fn(), + removeMember: jest.fn(), + listMembers: jest.fn(), + }; + identityUsers = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + TeamService, + { provide: ORGANISATION_TEAM_REPOSITORY, useValue: teams }, + { provide: IdentityUserService, useValue: identityUsers }, + ], + }).compile(); + + service = module.get(TeamService); + }); + + describe('create', () => { + it('persists the team with null fallbacks for description and headId', async () => { + teams.create.mockImplementation(async (t: TeamDomain) => t); + + await service.create({ title: 'Platform' }); + + const passed = teams.create.mock.calls[0][0] as TeamDomain; + expect(passed).toBeInstanceOf(TeamDomain); + expect(passed.description).toBeNull(); + expect(passed.headId).toBeNull(); + expect(identityUsers.getById).not.toHaveBeenCalled(); + }); + + it('verifies the head user exists when headId is supplied', async () => { + identityUsers.getById.mockResolvedValue(buildUser()); + teams.create.mockImplementation(async (t: TeamDomain) => t); + + await service.create({ title: 'Platform', headId: 200 }); + + expect(identityUsers.getById).toHaveBeenCalledWith(200); + }); + + it('skips the head verification when headId is null', async () => { + teams.create.mockImplementation(async (t: TeamDomain) => t); + + await service.create({ title: 'Platform', headId: null }); + + expect(identityUsers.getById).not.toHaveBeenCalled(); + }); + }); + + describe('search', () => { + it('delegates to the repository', async () => { + teams.search.mockResolvedValue([buildTeam()]); + const result = await service.search({}); + expect(teams.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('returns the team when found', async () => { + const team = buildTeam(); + teams.findById.mockResolvedValue(team); + await expect(service.getById(1)).resolves.toBe(team); + }); + + it('throws NotFoundException when missing', async () => { + teams.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('update', () => { + it('throws NotFoundException when the team does not exist', async () => { + teams.findById.mockResolvedValue(null); + await expect( + service.update(1, { title: 'X' }), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('verifies the head user when headId is supplied', async () => { + teams.findById.mockResolvedValue(buildTeam()); + identityUsers.getById.mockResolvedValue(buildUser()); + teams.updateById.mockResolvedValue(buildTeam()); + + await service.update(1, { headId: 200 }); + + expect(identityUsers.getById).toHaveBeenCalledWith(200); + }); + + it('skips head verification when headId is null', async () => { + teams.findById.mockResolvedValue(buildTeam()); + teams.updateById.mockResolvedValue(buildTeam()); + + await service.update(1, { headId: null }); + + expect(identityUsers.getById).not.toHaveBeenCalled(); + }); + + it('forwards the patch to the repository', async () => { + teams.findById.mockResolvedValue(buildTeam()); + teams.updateById.mockResolvedValue(buildTeam()); + + await service.update(1, { + title: 'New', + description: 'X', + }); + + expect(teams.updateById).toHaveBeenCalledWith(1, { + title: 'New', + description: 'X', + }); + }); + }); + + describe('delete', () => { + it('throws NotFoundException when the team does not exist', async () => { + teams.findById.mockResolvedValue(null); + await expect(service.delete(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('rejects when the team still has members', async () => { + teams.findById.mockResolvedValue(buildTeam()); + teams.listMembers.mockResolvedValue([buildMembership()]); + + await expect(service.delete(1)).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(teams.deleteById).not.toHaveBeenCalled(); + }); + + it('deletes the team when no members are linked', async () => { + teams.findById.mockResolvedValue(buildTeam()); + teams.listMembers.mockResolvedValue([]); + + await service.delete(1); + + expect(teams.deleteById).toHaveBeenCalledWith(1); + }); + }); + + describe('addMember', () => { + it('verifies the team and user, then adds the membership without user data by default', async () => { + teams.findById.mockResolvedValue(buildTeam()); + identityUsers.getById.mockResolvedValue(buildUser()); + const membership = buildMembership(); + teams.addMember.mockResolvedValue(membership); + + const result = await service.addMember(1, { + memberId: 200, + isPrimary: true, + } as any); + + expect(teams.addMember).toHaveBeenCalledWith(1, 200, true); + expect(result).toBe(membership); + expect(result.user).toBeUndefined(); + }); + + it('attaches the user to the membership when withUser is set', async () => { + teams.findById.mockResolvedValue(buildTeam()); + const user = buildUser(); + identityUsers.getById.mockResolvedValue(user); + teams.addMember.mockResolvedValue(buildMembership()); + + const result = await service.addMember( + 1, + { memberId: 200, isPrimary: true } as any, + { withUser: true }, + ); + + expect(result.user).toBe(user); + }); + }); + + describe('removeMember', () => { + it('verifies the team exists before delegating', async () => { + teams.findById.mockResolvedValue(buildTeam()); + + await service.removeMember(1, 200); + + expect(teams.removeMember).toHaveBeenCalledWith(1, 200); + }); + }); + + describe('listMembers', () => { + it('returns memberships without attached users when withUsers is false', async () => { + teams.findById.mockResolvedValue(buildTeam()); + const memberships = [ + buildMembership({ memberId: 200 }), + buildMembership({ memberId: 201 }), + ]; + teams.listMembers.mockResolvedValue(memberships); + + const result = await service.listMembers(1); + + expect(result).toBe(memberships); + expect(identityUsers.getById).not.toHaveBeenCalled(); + }); + + it('attaches users to every membership when withUsers is true', async () => { + teams.findById.mockResolvedValue(buildTeam()); + teams.listMembers.mockResolvedValue([ + buildMembership({ memberId: 200 }), + buildMembership({ memberId: 201 }), + ]); + identityUsers.getById.mockImplementation(async (id: number) => + buildUser(id), + ); + + const result = await service.listMembers(1, { withUsers: true }); + + expect(result).toHaveLength(2); + expect(result[0].user?.id).toBe(200); + expect(result[1].user?.id).toBe(201); + }); + + it('falls back to the bare membership when the user lookup fails', async () => { + teams.findById.mockResolvedValue(buildTeam()); + const original = buildMembership({ memberId: 200 }); + teams.listMembers.mockResolvedValue([original]); + identityUsers.getById.mockRejectedValue(new Error('boom')); + + const result = await service.listMembers(1, { withUsers: true }); + + // service catches the error and returns the original membership + expect(result[0]).toBe(original); + expect(result[0].user).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/domain/position-hierarchy.domain.spec.ts b/apps/api/test/unit/contexts/organisation/domain/position-hierarchy.domain.spec.ts new file mode 100644 index 00000000..5c40d83a --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/domain/position-hierarchy.domain.spec.ts @@ -0,0 +1,31 @@ +import { PositionHierarchyDomain } from 'src/contexts/organisation/domain/position-hierarchy.domain'; + +describe('PositionHierarchyDomain', () => { + describe('create', () => { + it('creates a hierarchy relation with every supplied field', () => { + const relation = PositionHierarchyDomain.create({ + id: 1, + superiorPositionId: 7, + subordinatePositionId: 8, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(relation.id).toBe(1); + expect(relation.superiorPositionId).toBe(7); + expect(relation.subordinatePositionId).toBe(8); + expect(relation.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('omits id and createdAt when not provided', () => { + const relation = PositionHierarchyDomain.create({ + superiorPositionId: 7, + subordinatePositionId: 8, + }); + + expect(relation.id).toBeUndefined(); + expect(relation.createdAt).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/domain/position.domain.spec.ts b/apps/api/test/unit/contexts/organisation/domain/position.domain.spec.ts new file mode 100644 index 00000000..3ca83d28 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/domain/position.domain.spec.ts @@ -0,0 +1,57 @@ +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; + +describe('PositionDomain', () => { + const baseProps = { title: 'Engineer' }; + + describe('create', () => { + it('creates a position with every supplied property', () => { + const position = PositionDomain.create({ + id: 1, + title: 'Engineer', + description: 'Software engineer', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(position.id).toBe(1); + expect(position.title).toBe('Engineer'); + expect(position.description).toBe('Software engineer'); + expect(position.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(position.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('normalises missing description to null', () => { + const position = PositionDomain.create(baseProps); + expect(position.description).toBeNull(); + }); + + it('preserves explicit null description', () => { + const position = PositionDomain.create({ + ...baseProps, + description: null, + }); + expect(position.description).toBeNull(); + }); + }); + + describe('withPatch', () => { + it('returns a new instance carrying the supplied patch', () => { + const position = PositionDomain.create({ + id: 1, + title: 'Engineer', + description: 'Old', + }); + + const next = position.withPatch({ description: 'New' }); + + expect(next).not.toBe(position); + expect(next.title).toBe('Engineer'); + expect(next.description).toBe('New'); + expect(position.description).toBe('Old'); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/domain/team-membership.domain.spec.ts b/apps/api/test/unit/contexts/organisation/domain/team-membership.domain.spec.ts new file mode 100644 index 00000000..8ed5b96c --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/domain/team-membership.domain.spec.ts @@ -0,0 +1,69 @@ +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { TeamMembershipDomain } from 'src/contexts/organisation/domain/team-membership.domain'; + +describe('TeamMembershipDomain', () => { + const baseProps = { teamId: 1, memberId: 200 }; + + describe('create', () => { + it('creates a membership with every supplied field', () => { + const user = UserDomain.create({ + id: 200, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + roles: [IdentityRole.EMPLOYEE], + }); + + const membership = TeamMembershipDomain.create({ + id: 1, + teamId: 1, + memberId: 200, + isPrimary: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + user, + }); + + expect(membership.id).toBe(1); + expect(membership.teamId).toBe(1); + expect(membership.memberId).toBe(200); + expect(membership.isPrimary).toBe(true); + expect(membership.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(membership.user).toBe(user); + }); + + it('defaults isPrimary to false when not supplied', () => { + const membership = TeamMembershipDomain.create(baseProps); + expect(membership.isPrimary).toBe(false); + }); + + it('coerces null isPrimary to false', () => { + const membership = TeamMembershipDomain.create({ + ...baseProps, + isPrimary: null, + }); + expect(membership.isPrimary).toBe(false); + }); + }); + + describe('withUser', () => { + it('returns a new instance carrying the supplied user', () => { + const membership = TeamMembershipDomain.create(baseProps); + const user = UserDomain.create({ + id: 200, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + }); + + const next = membership.withUser(user); + + expect(next).not.toBe(membership); + expect(next.user).toBe(user); + expect(membership.user).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/domain/team.domain.spec.ts b/apps/api/test/unit/contexts/organisation/domain/team.domain.spec.ts new file mode 100644 index 00000000..c67a1520 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/domain/team.domain.spec.ts @@ -0,0 +1,53 @@ +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; + +describe('TeamDomain', () => { + const baseProps = { title: 'Platform' }; + + describe('create', () => { + it('creates a team with every supplied property', () => { + const team = TeamDomain.create({ + id: 1, + title: 'Platform', + description: 'Platform engineering', + headId: 99, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + expect(team.id).toBe(1); + expect(team.title).toBe('Platform'); + expect(team.description).toBe('Platform engineering'); + expect(team.headId).toBe(99); + expect(team.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(team.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('defaults description and headId to null', () => { + const team = TeamDomain.create(baseProps); + expect(team.description).toBeNull(); + expect(team.headId).toBeNull(); + }); + }); + + describe('withPatch', () => { + it('returns a new instance carrying the supplied patch', () => { + const team = TeamDomain.create({ + id: 1, + title: 'Platform', + description: 'Old', + headId: 1, + }); + + const next = team.withPatch({ headId: 7 }); + + expect(next).not.toBe(team); + expect(next.headId).toBe(7); + expect(next.title).toBe('Platform'); + expect(team.headId).toBe(1); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/infrastructure/mappers/organisation.mapper.spec.ts b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/organisation.mapper.spec.ts new file mode 100644 index 00000000..76445fc3 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/organisation.mapper.spec.ts @@ -0,0 +1,89 @@ +import { + Position as PrismaPosition, + PositionHierarchy as PrismaPositionHierarchy, + Team as PrismaTeam, + TeamMembership as PrismaTeamMembership, +} from '@intra/database'; +import { PositionHierarchyDomain } from 'src/contexts/organisation/domain/position-hierarchy.domain'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { TeamMembershipDomain } from 'src/contexts/organisation/domain/team-membership.domain'; +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; +import { OrganisationMapper } from 'src/contexts/organisation/infrastructure/mappers/organisation.mapper'; + +describe('OrganisationMapper', () => { + describe('toTeamDomain', () => { + it('converts a prisma team row into a TeamDomain', () => { + const prismaTeam = { + id: 1, + title: 'Platform', + description: 'Platform engineering', + headId: 7, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaTeam; + + const domain = OrganisationMapper.toTeamDomain(prismaTeam); + + expect(domain).toBeInstanceOf(TeamDomain); + expect(domain.id).toBe(1); + expect(domain.title).toBe('Platform'); + expect(domain.headId).toBe(7); + }); + }); + + describe('toTeamMembershipDomain', () => { + it('converts a prisma membership row into a TeamMembershipDomain', () => { + const prismaRow = { + id: 1, + teamId: 10, + memberId: 200, + isPrimary: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaTeamMembership; + + const domain = OrganisationMapper.toTeamMembershipDomain(prismaRow); + + expect(domain).toBeInstanceOf(TeamMembershipDomain); + expect(domain.teamId).toBe(10); + expect(domain.memberId).toBe(200); + expect(domain.isPrimary).toBe(true); + }); + }); + + describe('toPositionDomain', () => { + it('converts a prisma position row into a PositionDomain', () => { + const prismaPosition = { + id: 1, + title: 'Engineer', + description: 'Software engineer', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaPosition; + + const domain = OrganisationMapper.toPositionDomain(prismaPosition); + + expect(domain).toBeInstanceOf(PositionDomain); + expect(domain.id).toBe(1); + expect(domain.title).toBe('Engineer'); + expect(domain.description).toBe('Software engineer'); + }); + }); + + describe('toPositionHierarchyDomain', () => { + it('converts a prisma hierarchy relation into a PositionHierarchyDomain', () => { + const prismaRow = { + id: 1, + superiorPositionId: 7, + subordinatePositionId: 8, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaPositionHierarchy; + + const domain = + OrganisationMapper.toPositionHierarchyDomain(prismaRow); + + expect(domain).toBeInstanceOf(PositionHierarchyDomain); + expect(domain.superiorPositionId).toBe(7); + expect(domain.subordinatePositionId).toBe(8); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/infrastructure/mappers/position-hierarchy.mapper.spec.ts b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/position-hierarchy.mapper.spec.ts new file mode 100644 index 00000000..c37c34b2 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/position-hierarchy.mapper.spec.ts @@ -0,0 +1,26 @@ +import { PositionHierarchy as PrismaPositionHierarchy } from '@intra/database'; +import { PositionHierarchyDomain } from 'src/contexts/organisation/domain/position-hierarchy.domain'; +import { PositionHierarchyMapper } from 'src/contexts/organisation/infrastructure/mappers/position-hierarchy.mapper'; + +describe('PositionHierarchyMapper', () => { + const prismaRow = { + id: 1, + superiorPositionId: 7, + subordinatePositionId: 8, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaPositionHierarchy; + + describe('toDomain', () => { + it('converts a prisma row into a PositionHierarchyDomain', () => { + const domain = PositionHierarchyMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(PositionHierarchyDomain); + expect(domain.id).toBe(1); + expect(domain.superiorPositionId).toBe(7); + expect(domain.subordinatePositionId).toBe(8); + expect(domain.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/infrastructure/mappers/position.mapper.spec.ts b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/position.mapper.spec.ts new file mode 100644 index 00000000..43551e80 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/position.mapper.spec.ts @@ -0,0 +1,56 @@ +import { Position as PrismaPosition } from '@intra/database'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionMapper } from 'src/contexts/organisation/infrastructure/mappers/position.mapper'; + +describe('PositionMapper', () => { + const prismaPosition = { + id: 1, + title: 'Engineer', + description: 'Software engineer', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaPosition; + + describe('toDomain', () => { + it('converts a prisma row into a PositionDomain', () => { + const domain = PositionMapper.toDomain(prismaPosition); + + expect(domain).toBeInstanceOf(PositionDomain); + expect(domain.id).toBe(1); + expect(domain.title).toBe('Engineer'); + expect(domain.description).toBe('Software engineer'); + }); + + it('preserves null description from prisma', () => { + const domain = PositionMapper.toDomain({ + ...prismaPosition, + description: null, + } as unknown as PrismaPosition); + expect(domain.description).toBeNull(); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input', () => { + const domain = PositionDomain.create({ + title: 'Engineer', + description: 'Software engineer', + }); + + const prisma = PositionMapper.toPrisma(domain); + + expect(prisma).toEqual({ + title: 'Engineer', + description: 'Software engineer', + }); + }); + + it('passes through null description when present on the domain', () => { + const domain = PositionDomain.create({ title: 'Engineer' }); + + const prisma = PositionMapper.toPrisma(domain); + + expect(prisma.description).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/infrastructure/mappers/team-membership.mapper.spec.ts b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/team-membership.mapper.spec.ts new file mode 100644 index 00000000..2aed61ea --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/team-membership.mapper.spec.ts @@ -0,0 +1,28 @@ +import { TeamMembership as PrismaTeamMembership } from '@intra/database'; +import { TeamMembershipDomain } from 'src/contexts/organisation/domain/team-membership.domain'; +import { TeamMembershipMapper } from 'src/contexts/organisation/infrastructure/mappers/team-membership.mapper'; + +describe('TeamMembershipMapper', () => { + const prismaRow = { + id: 1, + teamId: 10, + memberId: 200, + isPrimary: true, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaTeamMembership; + + describe('toDomain', () => { + it('converts a prisma row into a TeamMembershipDomain', () => { + const domain = TeamMembershipMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(TeamMembershipDomain); + expect(domain.id).toBe(1); + expect(domain.teamId).toBe(10); + expect(domain.memberId).toBe(200); + expect(domain.isPrimary).toBe(true); + expect(domain.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/infrastructure/mappers/team.mapper.spec.ts b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/team.mapper.spec.ts new file mode 100644 index 00000000..05d9f9b5 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/infrastructure/mappers/team.mapper.spec.ts @@ -0,0 +1,63 @@ +import { Team as PrismaTeam } from '@intra/database'; +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; +import { TeamMapper } from 'src/contexts/organisation/infrastructure/mappers/team.mapper'; + +describe('TeamMapper', () => { + const prismaTeam = { + id: 1, + title: 'Platform', + description: 'Platform engineering', + headId: 7, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + } as PrismaTeam; + + describe('toDomain', () => { + it('converts a prisma row into a TeamDomain', () => { + const domain = TeamMapper.toDomain(prismaTeam); + + expect(domain).toBeInstanceOf(TeamDomain); + expect(domain.id).toBe(1); + expect(domain.title).toBe('Platform'); + expect(domain.description).toBe('Platform engineering'); + expect(domain.headId).toBe(7); + }); + + it('preserves null description and headId from prisma', () => { + const domain = TeamMapper.toDomain({ + ...prismaTeam, + description: null, + headId: null, + } as unknown as PrismaTeam); + expect(domain.description).toBeNull(); + expect(domain.headId).toBeNull(); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain into a Prisma create input', () => { + const domain = TeamDomain.create({ + title: 'Platform', + description: 'desc', + headId: 7, + }); + + const prisma = TeamMapper.toPrisma(domain); + + expect(prisma).toEqual({ + title: 'Platform', + description: 'desc', + headId: 7, + }); + }); + + it('passes through null description and headId when present on the domain', () => { + const domain = TeamDomain.create({ title: 'Platform' }); + + const prisma = TeamMapper.toPrisma(domain); + + expect(prisma.description).toBeNull(); + expect(prisma.headId).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/presentation/controllers/positions.controller.spec.ts b/apps/api/test/unit/contexts/organisation/presentation/controllers/positions.controller.spec.ts new file mode 100644 index 00000000..60656499 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/presentation/controllers/positions.controller.spec.ts @@ -0,0 +1,156 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { PositionHierarchyService } from 'src/contexts/organisation/application/services/position-hierarchy.service'; +import { PositionService } from 'src/contexts/organisation/application/services/position.service'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionsController } from 'src/contexts/organisation/presentation/http/controllers/positions.controller'; + +function buildPosition(id = 1): PositionDomain { + return PositionDomain.create({ + id, + title: `Position ${id}`, + description: 'desc', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +describe('PositionsController', () => { + let controller: PositionsController; + let positions: any; + let hierarchy: any; + + beforeEach(() => { + positions = { + create: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + listCompetences: jest.fn(), + }; + hierarchy = { + link: jest.fn(), + unlink: jest.fn(), + listSubordinates: jest.fn(), + listSuperiors: jest.fn(), + }; + + controller = new PositionsController( + positions as unknown as PositionService, + hierarchy as unknown as PositionHierarchyService, + ); + }); + + describe('create', () => { + it('forwards the dto to the service and maps the response', async () => { + positions.create.mockResolvedValue(buildPosition()); + + const result = await controller.create({ + title: 'Engineer', + description: 'desc', + } as any); + + expect(positions.create).toHaveBeenCalledWith({ + title: 'Engineer', + description: 'desc', + }); + expect(result.id).toBe(1); + }); + }); + + describe('search', () => { + it('maps every position to a response', async () => { + positions.search.mockResolvedValue([buildPosition()]); + const result = await controller.search({} as any); + expect(positions.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('parses the id and forwards to the service', async () => { + positions.getById.mockResolvedValue(buildPosition()); + const result = await controller.getById('1'); + expect(positions.getById).toHaveBeenCalledWith(1); + expect(result.id).toBe(1); + }); + }); + + describe('update', () => { + it('parses the id and forwards the dto', async () => { + positions.update.mockResolvedValue(buildPosition()); + await controller.update('1', { title: 'X' } as any); + expect(positions.update).toHaveBeenCalledWith(1, { title: 'X' }); + }); + }); + + describe('delete', () => { + it('parses the id and forwards to the service', async () => { + await controller.delete('1'); + expect(positions.delete).toHaveBeenCalledWith(1); + }); + }); + + describe('linkSubordinate', () => { + it('links and returns the subordinate position', async () => { + hierarchy.link.mockResolvedValue(undefined); + positions.getById.mockResolvedValue(buildPosition(2)); + + const result = await controller.linkSubordinate('1', { + subordinateId: 2, + } as any); + + expect(hierarchy.link).toHaveBeenCalledWith(1, 2); + expect(positions.getById).toHaveBeenCalledWith(2); + expect(result.id).toBe(2); + }); + }); + + describe('unlinkSubordinate', () => { + it('parses both ids and forwards the call', async () => { + await controller.unlinkSubordinate('1', '2'); + expect(hierarchy.unlink).toHaveBeenCalledWith(1, 2); + }); + }); + + describe('listSubordinates', () => { + it('maps every subordinate to a response', async () => { + hierarchy.listSubordinates.mockResolvedValue([ + buildPosition(2), + buildPosition(3), + ]); + + const result = await controller.listSubordinates('1'); + + expect(hierarchy.listSubordinates).toHaveBeenCalledWith(1); + expect(result.map((r) => r.id)).toEqual([2, 3]); + }); + }); + + describe('listSuperiors', () => { + it('maps every superior to a response', async () => { + hierarchy.listSuperiors.mockResolvedValue([ + buildPosition(7), + buildPosition(8), + ]); + + const result = await controller.listSuperiors('1'); + + expect(hierarchy.listSuperiors).toHaveBeenCalledWith(1); + expect(result.map((r) => r.id)).toEqual([7, 8]); + }); + }); + + describe('listCompetences', () => { + it('forwards the call and returns the list', async () => { + positions.listCompetences.mockResolvedValue([7, 8]); + const result = await controller.listCompetences('1'); + expect(positions.listCompetences).toHaveBeenCalledWith(1); + expect(result).toEqual([7, 8]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/presentation/controllers/teams.controller.spec.ts b/apps/api/test/unit/contexts/organisation/presentation/controllers/teams.controller.spec.ts new file mode 100644 index 00000000..a7701894 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/presentation/controllers/teams.controller.spec.ts @@ -0,0 +1,144 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { TeamService } from 'src/contexts/organisation/application/services/team.service'; +import { TeamMembershipDomain } from 'src/contexts/organisation/domain/team-membership.domain'; +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; +import { TeamsController } from 'src/contexts/organisation/presentation/http/controllers/teams.controller'; + +function buildTeam(): TeamDomain { + return TeamDomain.create({ + id: 1, + title: 'Platform', + description: 'desc', + headId: 7, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); +} + +function buildMembership(memberId = 200): TeamMembershipDomain { + return TeamMembershipDomain.create({ + id: memberId, + teamId: 1, + memberId, + isPrimary: false, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); +} + +describe('TeamsController', () => { + let controller: TeamsController; + let service: any; + + beforeEach(() => { + service = { + create: jest.fn(), + search: jest.fn(), + getById: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + addMember: jest.fn(), + removeMember: jest.fn(), + listMembers: jest.fn(), + }; + + controller = new TeamsController(service as unknown as TeamService); + }); + + describe('create', () => { + it('forwards the dto and maps the response', async () => { + service.create.mockResolvedValue(buildTeam()); + + const result = await controller.create({ + title: 'Platform', + description: 'desc', + headId: 7, + } as any); + + expect(service.create).toHaveBeenCalledWith({ + title: 'Platform', + description: 'desc', + headId: 7, + }); + expect(result.id).toBe(1); + }); + }); + + describe('search', () => { + it('maps every team to a response', async () => { + service.search.mockResolvedValue([buildTeam()]); + const result = await controller.search({} as any); + expect(service.search).toHaveBeenCalledWith({}); + expect(result).toHaveLength(1); + }); + }); + + describe('getById', () => { + it('parses the id and forwards to the service', async () => { + service.getById.mockResolvedValue(buildTeam()); + const result = await controller.getById('1'); + expect(service.getById).toHaveBeenCalledWith(1); + expect(result.id).toBe(1); + }); + }); + + describe('update', () => { + it('parses the id and forwards the dto', async () => { + service.update.mockResolvedValue(buildTeam()); + await controller.update('1', { title: 'X' } as any); + expect(service.update).toHaveBeenCalledWith(1, { title: 'X' }); + }); + }); + + describe('delete', () => { + it('parses the id and forwards to the service', async () => { + await controller.delete('1'); + expect(service.delete).toHaveBeenCalledWith(1); + }); + }); + + describe('addMember', () => { + it('parses the id, forwards the dto with withUser, and maps the response', async () => { + service.addMember.mockResolvedValue(buildMembership()); + + const result = await controller.addMember('1', { + memberId: 200, + isPrimary: true, + } as any); + + expect(service.addMember).toHaveBeenCalledWith( + 1, + { memberId: 200, isPrimary: true }, + { withUser: true }, + ); + expect(result.memberId).toBe(200); + }); + }); + + describe('listMembers', () => { + it('maps every membership to a response and requests user data', async () => { + service.listMembers.mockResolvedValue([ + buildMembership(200), + buildMembership(201), + ]); + + const result = await controller.listMembers('1'); + + expect(service.listMembers).toHaveBeenCalledWith(1, { + withUsers: true, + }); + expect(result).toHaveLength(2); + expect(result.map((r) => r.memberId)).toEqual([200, 201]); + }); + }); + + describe('removeMember', () => { + it('parses both ids and forwards the call', async () => { + await controller.removeMember('1', '200'); + expect(service.removeMember).toHaveBeenCalledWith(1, 200); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/presentation/mappers/position.http.mapper.spec.ts b/apps/api/test/unit/contexts/organisation/presentation/mappers/position.http.mapper.spec.ts new file mode 100644 index 00000000..3950eb90 --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/presentation/mappers/position.http.mapper.spec.ts @@ -0,0 +1,34 @@ +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { PositionHttpMapper } from 'src/contexts/organisation/presentation/http/mappers/position.http.mapper'; + +describe('PositionHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = PositionDomain.create({ + id: 1, + title: 'Engineer', + description: 'desc', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = PositionHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.title).toBe('Engineer'); + expect(response.description).toBe('desc'); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('passes null description through to the response', () => { + const domain = PositionDomain.create({ id: 1, title: 'Engineer' }); + const response = PositionHttpMapper.toResponse(domain); + expect(response.description).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/presentation/mappers/team-membership.http.mapper.spec.ts b/apps/api/test/unit/contexts/organisation/presentation/mappers/team-membership.http.mapper.spec.ts new file mode 100644 index 00000000..812a3a2b --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/presentation/mappers/team-membership.http.mapper.spec.ts @@ -0,0 +1,55 @@ +import { IdentityRole, IdentityStatus } from '@intra/shared-kernel'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { TeamMembershipDomain } from 'src/contexts/organisation/domain/team-membership.domain'; +import { TeamMembershipHttpMapper } from 'src/contexts/organisation/presentation/http/mappers/team-membership.http.mapper'; + +describe('TeamMembershipHttpMapper', () => { + describe('toResponse', () => { + it('maps the basic membership without the user when none is set', () => { + const domain = TeamMembershipDomain.create({ + id: 1, + teamId: 10, + memberId: 200, + isPrimary: false, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = TeamMembershipHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.teamId).toBe(10); + expect(response.memberId).toBe(200); + expect(response.isPrimary).toBe(false); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.user).toBeUndefined(); + }); + + it('includes the user when the domain carries one', () => { + const user = UserDomain.create({ + id: 200, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + status: IdentityStatus.ACTIVE, + roles: [IdentityRole.EMPLOYEE], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-01T00:00:00.000Z'), + }); + const domain = TeamMembershipDomain.create({ + id: 1, + teamId: 10, + memberId: 200, + isPrimary: true, + user, + }); + + const response = TeamMembershipHttpMapper.toResponse(domain); + + expect(response.user).toBeDefined(); + expect(response.user!.id).toBe(200); + expect(response.user!.fullName).toBe('Jane Doe'); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/organisation/presentation/mappers/team.http.mapper.spec.ts b/apps/api/test/unit/contexts/organisation/presentation/mappers/team.http.mapper.spec.ts new file mode 100644 index 00000000..3120788c --- /dev/null +++ b/apps/api/test/unit/contexts/organisation/presentation/mappers/team.http.mapper.spec.ts @@ -0,0 +1,37 @@ +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; +import { TeamHttpMapper } from 'src/contexts/organisation/presentation/http/mappers/team.http.mapper'; + +describe('TeamHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = TeamDomain.create({ + id: 1, + title: 'Platform', + description: 'desc', + headId: 7, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + updatedAt: new Date('2025-01-02T00:00:00.000Z'), + }); + + const response = TeamHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.title).toBe('Platform'); + expect(response.description).toBe('desc'); + expect(response.headId).toBe(7); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + expect(response.updatedAt).toEqual( + new Date('2025-01-02T00:00:00.000Z'), + ); + }); + + it('passes null description and headId through to the response', () => { + const domain = TeamDomain.create({ id: 1, title: 'Platform' }); + const response = TeamHttpMapper.toResponse(domain); + expect(response.description).toBeNull(); + expect(response.headId).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/listeners/cycle-stage.listener.spec.ts b/apps/api/test/unit/contexts/reporting/application/listeners/cycle-stage.listener.spec.ts new file mode 100644 index 00000000..f09988bb --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/listeners/cycle-stage.listener.spec.ts @@ -0,0 +1,107 @@ +import { CycleStage } from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { CycleStageProcessedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-processed.event'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service'; +import { CycleStageListener } from 'src/contexts/reporting/application/listeners/cycle-stage.listener'; +import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('CycleStageListener (reporting)', () => { + let listener: CycleStageListener; + let strategicReportingService: any; + let cycleService: any; + let reviews: any; + + beforeEach(async () => { + jest.useFakeTimers(); + strategicReportingService = { + generateStrategicReportForCycle: jest.fn(), + }; + cycleService = { changeStage: jest.fn() }; + reviews = { listByCycleId: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + CycleStageListener, + { + provide: StrategicReportingService, + useValue: strategicReportingService, + }, + { provide: CycleService, useValue: cycleService }, + { provide: REVIEW_REPOSITORY, useValue: reviews }, + ], + }).compile(); + + listener = module.get(CycleStageListener); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('ignores transitions that are not FINISHED', async () => { + const event = new CycleStageProcessedEvent(1, CycleStage.ACTIVE); + await listener.handleCycleStageProcessed(event); + + expect(cycleService.changeStage).not.toHaveBeenCalled(); + expect( + strategicReportingService.generateStrategicReportForCycle, + ).not.toHaveBeenCalled(); + }); + + it('runs the report pipeline when FINISHED: PREPARING_REPORT → generate → PUBLISHED', async () => { + strategicReportingService.generateStrategicReportForCycle.mockResolvedValue( + { id: 99 }, + ); + reviews.listByCycleId.mockResolvedValue([{ id: 1 }]); + + const event = new CycleStageProcessedEvent(1, CycleStage.FINISHED); + await listener.handleCycleStageProcessed(event); + + expect(cycleService.changeStage).toHaveBeenNthCalledWith( + 1, + 1, + CycleStage.PREPARING_REPORT, + ); + expect( + strategicReportingService.generateStrategicReportForCycle, + ).toHaveBeenCalledWith(1); + expect(cycleService.changeStage).toHaveBeenNthCalledWith( + 2, + 1, + CycleStage.PUBLISHED, + ); + }); + + it('swallows errors from the report generation pipeline', async () => { + strategicReportingService.generateStrategicReportForCycle.mockRejectedValue( + new Error('boom'), + ); + + const event = new CycleStageProcessedEvent(1, CycleStage.FINISHED); + await expect( + listener.handleCycleStageProcessed(event), + ).resolves.toBeUndefined(); + + // Only the first stage change happens before failure + expect(cycleService.changeStage).toHaveBeenCalledWith( + 1, + CycleStage.PREPARING_REPORT, + ); + // PUBLISHED is never reached because generation threw + expect(cycleService.changeStage).not.toHaveBeenCalledWith( + 1, + CycleStage.PUBLISHED, + ); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/listeners/review-stage.listener.spec.ts b/apps/api/test/unit/contexts/reporting/application/listeners/review-stage.listener.spec.ts new file mode 100644 index 00000000..9414dd81 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/listeners/review-stage.listener.spec.ts @@ -0,0 +1,84 @@ +import { ReviewStage } from '@intra/shared-kernel'; +import { Logger } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ReviewStageProcessedEvent } from 'src/contexts/feedback360/application/events/review-stage-processed.event'; +import { ReviewService } from 'src/contexts/feedback360/application/services/review.service'; +import { ReviewStageListener } from 'src/contexts/reporting/application/listeners/review-stage.listener'; +import { ReportingService } from 'src/contexts/reporting/application/services/reports.service'; + +beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + jest.spyOn(Logger.prototype, 'warn').mockImplementation(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('ReviewStageListener (reporting)', () => { + let listener: ReviewStageListener; + let reportService: any; + let reviewService: any; + + beforeEach(async () => { + reportService = { generateReportForReview: jest.fn() }; + reviewService = { changeReviewStage: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + ReviewStageListener, + { provide: ReportingService, useValue: reportService }, + { provide: ReviewService, useValue: reviewService }, + ], + }).compile(); + + listener = module.get(ReviewStageListener); + }); + + it('ignores transitions that are not FINISHED', async () => { + const event = new ReviewStageProcessedEvent(1, ReviewStage.IN_PROGRESS); + await listener.handleReviewStageProcessed(event); + + expect(reviewService.changeReviewStage).not.toHaveBeenCalled(); + expect(reportService.generateReportForReview).not.toHaveBeenCalled(); + }); + + it('runs the report pipeline when FINISHED: PREPARING_REPORT → generate → PROCESSING_BY_HR', async () => { + reportService.generateReportForReview.mockResolvedValue({ id: 99 }); + + const event = new ReviewStageProcessedEvent(1, ReviewStage.FINISHED); + await listener.handleReviewStageProcessed(event); + + expect(reviewService.changeReviewStage).toHaveBeenNthCalledWith( + 1, + 1, + ReviewStage.PREPARING_REPORT, + ); + expect(reportService.generateReportForReview).toHaveBeenCalledWith(1); + expect(reviewService.changeReviewStage).toHaveBeenNthCalledWith( + 2, + 1, + ReviewStage.PROCESSING_BY_HR, + ); + }); + + it('swallows errors from the report generation pipeline', async () => { + reportService.generateReportForReview.mockRejectedValue( + new Error('boom'), + ); + + const event = new ReviewStageProcessedEvent(1, ReviewStage.FINISHED); + await expect( + listener.handleReviewStageProcessed(event), + ).resolves.toBeUndefined(); + + expect(reviewService.changeReviewStage).toHaveBeenCalledWith( + 1, + ReviewStage.PREPARING_REPORT, + ); + expect(reviewService.changeReviewStage).not.toHaveBeenCalledWith( + 1, + ReviewStage.PROCESSING_BY_HR, + ); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/services/report-analytics.service.spec.ts b/apps/api/test/unit/contexts/reporting/application/services/report-analytics.service.spec.ts new file mode 100644 index 00000000..90bd574d --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/services/report-analytics.service.spec.ts @@ -0,0 +1,84 @@ +import { EntityType } from '@intra/shared-kernel'; +import { NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { REPORT_ANALYTICS_REPOSITORY } from 'src/contexts/reporting/application/ports/report-analytics.repository.port'; +import { ReportAnalyticsService } from 'src/contexts/reporting/application/services/report-analytics.service'; +import { ReportingService } from 'src/contexts/reporting/application/services/reports.service'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; + +function buildAnalytics(reportId = 10): ReportAnalyticsDomain { + return ReportAnalyticsDomain.create({ + id: 1, + reportId, + entityType: EntityType.QUESTION, + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + }); +} + +describe('ReportAnalyticsService', () => { + let service: ReportAnalyticsService; + let analytics: any; + let reporting: any; + + beforeEach(async () => { + analytics = { + findByReportId: jest.fn(), + findById: jest.fn(), + }; + reporting = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + ReportAnalyticsService, + { provide: REPORT_ANALYTICS_REPOSITORY, useValue: analytics }, + { provide: ReportingService, useValue: reporting }, + ], + }).compile(); + + service = module.get(ReportAnalyticsService); + }); + + describe('getByReportId', () => { + it('returns analytics after the access check passes', async () => { + reporting.getById.mockResolvedValue({ id: 10 }); + const rows = [buildAnalytics()]; + analytics.findByReportId.mockResolvedValue(rows); + + await expect(service.getByReportId(10)).resolves.toBe(rows); + expect(reporting.getById).toHaveBeenCalledWith(10, undefined); + }); + + it('throws NotFoundException when repository returns null', async () => { + reporting.getById.mockResolvedValue({ id: 10 }); + analytics.findByReportId.mockResolvedValue(null); + + await expect(service.getByReportId(10)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('getById', () => { + it('throws NotFoundException when the analytics row is missing', async () => { + analytics.findById.mockResolvedValue(null); + + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('re-checks access via the parent report', async () => { + const row = buildAnalytics(99); + analytics.findById.mockResolvedValue(row); + reporting.getById.mockResolvedValue({ id: 99 }); + + const result = await service.getById(1); + + expect(reporting.getById).toHaveBeenCalledWith(99, undefined); + expect(result).toBe(row); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/services/report-comment.service.spec.ts b/apps/api/test/unit/contexts/reporting/application/services/report-comment.service.spec.ts new file mode 100644 index 00000000..99335ee3 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/services/report-comment.service.spec.ts @@ -0,0 +1,100 @@ +import { RespondentCategory } from '@intra/shared-kernel'; +import { NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { REPORT_COMMENT_REPOSITORY } from 'src/contexts/reporting/application/ports/report-comment.repository.port'; +import { ReportCommentService } from 'src/contexts/reporting/application/services/report-comment.service'; +import { ReportingService } from 'src/contexts/reporting/application/services/reports.service'; +import { ReportCommentDomain } from 'src/contexts/reporting/domain/report-comment.domain'; + +function buildComment(): ReportCommentDomain { + return ReportCommentDomain.create({ + id: 1, + reportId: 10, + questionId: 20, + questionTitle: 'Q?', + comment: 'Nice', + respondentCategories: [RespondentCategory.TEAM], + numberOfMentions: 1, + }); +} + +describe('ReportCommentService', () => { + let service: ReportCommentService; + let comments: any; + let reporting: any; + + beforeEach(async () => { + comments = { + findByReportId: jest.fn(), + findById: jest.fn(), + create: jest.fn(), + }; + reporting = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + ReportCommentService, + { provide: REPORT_COMMENT_REPOSITORY, useValue: comments }, + { provide: ReportingService, useValue: reporting }, + ], + }).compile(); + + service = module.get(ReportCommentService); + }); + + describe('getByReportId', () => { + it('returns comments after asserting access via ReportingService', async () => { + reporting.getById.mockResolvedValue({ id: 10 }); + comments.findByReportId.mockResolvedValue([buildComment()]); + + const result = await service.getByReportId(10); + + expect(reporting.getById).toHaveBeenCalledWith(10, undefined); + expect(comments.findByReportId).toHaveBeenCalledWith(10); + expect(result).toHaveLength(1); + }); + + it('throws NotFoundException when no comments are found', async () => { + reporting.getById.mockResolvedValue({ id: 10 }); + comments.findByReportId.mockResolvedValue([]); + + await expect(service.getByReportId(10)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('propagates errors from the reporting access check', async () => { + reporting.getById.mockRejectedValue(new NotFoundException('x')); + + await expect(service.getByReportId(10)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('getById', () => { + it('returns the comment when found', async () => { + const comment = buildComment(); + comments.findById.mockResolvedValue(comment); + + await expect(service.getById(1)).resolves.toBe(comment); + }); + + it('throws NotFoundException when missing', async () => { + comments.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('create', () => { + it('delegates to the repository', async () => { + const comment = buildComment(); + comments.create.mockResolvedValue(comment); + + await expect(service.create(comment)).resolves.toBe(comment); + expect(comments.create).toHaveBeenCalledWith(comment); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/services/report-insight.service.spec.ts b/apps/api/test/unit/contexts/reporting/application/services/report-insight.service.spec.ts new file mode 100644 index 00000000..a5141bd1 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/services/report-insight.service.spec.ts @@ -0,0 +1,83 @@ +import { EntityType, InsightType } from '@intra/shared-kernel'; +import { NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { REPORT_INSIGHT_REPOSITORY } from 'src/contexts/reporting/application/ports/report-insight.repository.port'; +import { ReportInsightService } from 'src/contexts/reporting/application/services/report-insight.service'; +import { ReportingService } from 'src/contexts/reporting/application/services/reports.service'; +import { ReportInsightDomain } from 'src/contexts/reporting/domain/report-insight.domain'; + +function buildInsight(reportId = 10): ReportInsightDomain { + return ReportInsightDomain.create({ + id: 1, + reportId, + insightType: InsightType.HIGHEST_RATING, + entityType: EntityType.QUESTION, + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + }); +} + +describe('ReportInsightService', () => { + let service: ReportInsightService; + let insights: any; + let reporting: any; + + beforeEach(async () => { + insights = { + findByReportId: jest.fn(), + findById: jest.fn(), + }; + reporting = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + ReportInsightService, + { provide: REPORT_INSIGHT_REPOSITORY, useValue: insights }, + { provide: ReportingService, useValue: reporting }, + ], + }).compile(); + + service = module.get(ReportInsightService); + }); + + describe('getByReportId', () => { + it('returns insights after the access check passes', async () => { + reporting.getById.mockResolvedValue({ id: 10 }); + const rows = [buildInsight()]; + insights.findByReportId.mockResolvedValue(rows); + + await expect(service.getByReportId(10)).resolves.toBe(rows); + }); + + it('throws NotFoundException when the repo returns null', async () => { + reporting.getById.mockResolvedValue({ id: 10 }); + insights.findByReportId.mockResolvedValue(null); + + await expect(service.getByReportId(10)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + }); + + describe('getById', () => { + it('throws NotFoundException when the insight is missing', async () => { + insights.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('re-checks access via the parent report', async () => { + const insight = buildInsight(50); + insights.findById.mockResolvedValue(insight); + reporting.getById.mockResolvedValue({ id: 50 }); + + const result = await service.getById(1); + + expect(reporting.getById).toHaveBeenCalledWith(50, undefined); + expect(result).toBe(insight); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/services/reports.service.spec.ts b/apps/api/test/unit/contexts/reporting/application/services/reports.service.spec.ts new file mode 100644 index 00000000..d8a49fc9 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/services/reports.service.spec.ts @@ -0,0 +1,383 @@ +import { + AnswerType, + IdentityRole, + RespondentCategory, +} from '@intra/shared-kernel'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ANSWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/answer.repository.port'; +import { CLUSTER_SCORE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cluster-score.repository.port'; +import { CYCLE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle.repository.port'; +import { RESPONDENT_REPOSITORY } from 'src/contexts/feedback360/application/ports/respondent.repository.port'; +import { REVIEW_QUESTION_RELATION_REPOSITORY } from 'src/contexts/feedback360/application/ports/review-question-relation.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { REVIEWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/reviewer.repository.port'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { CLUSTER_REPOSITORY } from 'src/contexts/library/application/ports/cluster.repository.port'; +import { ClusterDomain } from 'src/contexts/library/domain/cluster.domain'; +import { REPORT_ANALYTICS_REPOSITORY } from 'src/contexts/reporting/application/ports/report-analytics.repository.port'; +import { REPORT_COMMENT_REPOSITORY } from 'src/contexts/reporting/application/ports/report-comment.repository.port'; +import { REPORT_INSIGHT_REPOSITORY } from 'src/contexts/reporting/application/ports/report-insight.repository.port'; +import { REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/report.repository.port'; +import { ReportingService } from 'src/contexts/reporting/application/services/reports.service'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 10, + rateeId: 200, + rateeFullName: 'Ratee', + rateePositionId: 1, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + cycleId: 100, + ...overrides, + }); +} + +function buildUser( + overrides: Partial[0]> = {}, +): UserDomain { + return UserDomain.create({ + id: 5, + firstName: 'A', + lastName: 'B', + email: 'a@b.com', + roles: [IdentityRole.EMPLOYEE], + ...overrides, + }); +} + +function buildReport(reviewId = 10): ReportDomain { + return ReportDomain.create({ + id: 1, + reviewId, + cycleId: 100, + respondentCount: 0, + respondentCategories: [], + answerCount: 0, + analytics: [], + }); +} + +describe('ReportingService', () => { + let service: ReportingService; + let reports: any; + let analyticsRepo: any; + let insightsRepo: any; + let commentsRepo: any; + let respondents: any; + let reviewers: any; + let answers: any; + let reviewQuestionRelations: any; + let reviews: any; + let clusters: any; + let clusterScores: any; + let cyclesRepo: any; + + beforeEach(async () => { + reports = { + search: jest.fn(), + findById: jest.fn(), + findByReviewId: jest.fn(), + create: jest.fn(), + }; + analyticsRepo = { + createMany: jest.fn(), + findByReportId: jest.fn(), + }; + insightsRepo = { createMany: jest.fn() }; + commentsRepo = { create: jest.fn() }; + respondents = { listByReview: jest.fn() }; + reviewers = { listByReview: jest.fn() }; + answers = { + list: jest.fn(), + getAnswersCountByRespondentCategories: jest.fn(), + }; + reviewQuestionRelations = { listByReview: jest.fn() }; + reviews = { + findById: jest.fn(), + updateById: jest.fn(), + }; + clusters = { search: jest.fn() }; + clusterScores = { upsert: jest.fn() }; + cyclesRepo = { findById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + ReportingService, + { provide: REPORT_REPOSITORY, useValue: reports }, + { + provide: REPORT_ANALYTICS_REPOSITORY, + useValue: analyticsRepo, + }, + { + provide: REPORT_INSIGHT_REPOSITORY, + useValue: insightsRepo, + }, + { + provide: REPORT_COMMENT_REPOSITORY, + useValue: commentsRepo, + }, + { provide: RESPONDENT_REPOSITORY, useValue: respondents }, + { provide: REVIEWER_REPOSITORY, useValue: reviewers }, + { provide: ANSWER_REPOSITORY, useValue: answers }, + { + provide: REVIEW_QUESTION_RELATION_REPOSITORY, + useValue: reviewQuestionRelations, + }, + { provide: REVIEW_REPOSITORY, useValue: reviews }, + { provide: CLUSTER_REPOSITORY, useValue: clusters }, + { provide: CLUSTER_SCORE_REPOSITORY, useValue: clusterScores }, + { provide: CYCLE_REPOSITORY, useValue: cyclesRepo }, + ], + }).compile(); + + service = module.get(ReportingService); + }); + + describe('search', () => { + it('delegates to the repository when no actor is supplied', async () => { + reports.search.mockResolvedValue([buildReport()]); + const result = await service.search({}); + expect(result).toHaveLength(1); + expect(reports.search).toHaveBeenCalledWith({}); + }); + + it('throws ForbiddenException for non-admin/HR actors', async () => { + await expect( + service.search( + {}, + buildUser({ roles: [IdentityRole.EMPLOYEE] }), + ), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + + it('allows admin/HR actors', async () => { + reports.search.mockResolvedValue([]); + await expect( + service.search({}, buildUser({ roles: [IdentityRole.HR] })), + ).resolves.toEqual([]); + }); + }); + + describe('getById', () => { + it('throws NotFoundException when the report is missing', async () => { + reports.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('returns the report for HR actors', async () => { + const report = buildReport(); + reports.findById.mockResolvedValue(report); + + const result = await service.getById( + 1, + buildUser({ roles: [IdentityRole.HR] }), + ); + expect(result).toBe(report); + }); + + it('returns the report when the actor is the ratee', async () => { + const report = buildReport(); + reports.findById.mockResolvedValue(report); + reviews.findById.mockResolvedValue(buildReview({ rateeId: 5 })); + + const result = await service.getById( + 1, + buildUser({ id: 5, roles: [IdentityRole.EMPLOYEE] }), + ); + expect(result).toBe(report); + }); + + it('returns the report when the actor is a reviewer', async () => { + const report = buildReport(); + reports.findById.mockResolvedValue(report); + reviews.findById.mockResolvedValue(buildReview({ rateeId: 200 })); + reviewers.listByReview.mockResolvedValue([{ id: 1 }]); + + const result = await service.getById( + 1, + buildUser({ id: 999, roles: [IdentityRole.EMPLOYEE] }), + ); + expect(result).toBe(report); + }); + + it('throws ForbiddenException when the actor has no relation', async () => { + const report = buildReport(); + reports.findById.mockResolvedValue(report); + reviews.findById.mockResolvedValue(buildReview({ rateeId: 200 })); + reviewers.listByReview.mockResolvedValue([]); + + await expect( + service.getById( + 1, + buildUser({ id: 999, roles: [IdentityRole.EMPLOYEE] }), + ), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + + it('throws NotFoundException when the underlying review is missing', async () => { + const report = buildReport(); + reports.findById.mockResolvedValue(report); + reviews.findById.mockResolvedValue(null); + + await expect( + service.getById( + 1, + buildUser({ id: 999, roles: [IdentityRole.EMPLOYEE] }), + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('getByReviewId', () => { + it('throws NotFoundException when no report exists for the review', async () => { + reports.findByReviewId.mockResolvedValue(null); + await expect(service.getByReviewId(10)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('returns the report after checking access', async () => { + const report = buildReport(); + reports.findByReviewId.mockResolvedValue(report); + + const result = await service.getByReviewId(10); + expect(result).toBe(report); + }); + }); + + describe('generateReportForReview', () => { + beforeEach(() => { + process.env.NODE_ENV = 'test'; + }); + + it('returns the existing report when one is already stored', async () => { + const existing = buildReport(); + reports.findByReviewId.mockResolvedValue(existing); + + const result = await service.generateReportForReview(10); + + expect(result).toBe(existing); + expect(reports.create).not.toHaveBeenCalled(); + }); + + it('throws when the underlying review is missing', async () => { + reports.findByReviewId.mockResolvedValue(null); + reviews.findById.mockResolvedValue(null); + + await expect( + service.generateReportForReview(10), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('generates a report with analytics, insights and updates the review', async () => { + reports.findByReviewId.mockResolvedValue(null); + reviews.findById.mockResolvedValue(buildReview()); + respondents.listByReview.mockResolvedValue([ + RespondentDomain.create({ + reviewId: 10, + respondentId: 11, + fullName: 'A', + category: RespondentCategory.TEAM, + positionId: 1, + positionTitle: 'Eng', + }), + ]); + answers.getAnswersCountByRespondentCategories.mockResolvedValue([ + { respondentCategory: RespondentCategory.TEAM, answers: 5 }, + ]); + const review = buildReview(); + const answerRows = [ + AnswerDomain.create({ + reviewId: 10, + questionId: 100, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }), + AnswerDomain.create({ + reviewId: 10, + questionId: 100, + respondentCategory: RespondentCategory.SELF_ASSESSMENT, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 5, + }), + ]; + answers.list.mockResolvedValue(answerRows); + reviewQuestionRelations.listByReview.mockResolvedValue([ + ReviewQuestionRelationDomain.create({ + reviewId: 10, + questionId: 100, + questionTitle: 'Q?', + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 7, + competenceTitle: 'Teamwork', + }), + ]); + clusters.search.mockResolvedValue([ + ClusterDomain.create({ + id: 1, + competenceId: 7, + lowerBound: 0, + upperBound: 5, + title: 'High', + description: 'desc', + }), + ]); + reports.create.mockImplementation(async (r: ReportDomain) => + ReportDomain.create({ ...r, id: 42, analytics: [] }), + ); + + const result = await service.generateReportForReview(10); + + expect(result.id).toBe(42); + // Two analytics rows: 1 question + 1 competence + expect(analyticsRepo.createMany).toHaveBeenCalledTimes(1); + const createdAnalytics = analyticsRepo.createMany.mock.calls[0][1]; + expect(createdAnalytics).toHaveLength(2); + // Four insights: HIGHEST/LOWEST rating + delta per entity type + const createdInsights = insightsRepo.createMany.mock.calls[0][1]; + expect(createdInsights).toHaveLength(8); // 4 question + 4 competence + expect(reviews.updateById).toHaveBeenCalledWith(10, { + reportId: 42, + }); + // Cluster score upserted because competence has a numerical answer + expect(clusterScores.upsert).toHaveBeenCalledTimes(1); + }); + + it('throws when only self-assessment answers exist in production mode', async () => { + // The service caches NODE_ENV at construction time via a private + // field initializer; flip it directly for this single test. + (service as any).isProduction = true; + + reports.findByReviewId.mockResolvedValue(null); + reviews.findById.mockResolvedValue(buildReview()); + respondents.listByReview.mockResolvedValue([]); + answers.getAnswersCountByRespondentCategories.mockResolvedValue([ + { + respondentCategory: RespondentCategory.SELF_ASSESSMENT, + answers: 1, + }, + ]); + cyclesRepo.findById.mockResolvedValue({ + id: 100, + minRespondentsThreshold: 3, + }); + + await expect( + service.generateReportForReview(10), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/services/strategic-report-analytics.service.spec.ts b/apps/api/test/unit/contexts/reporting/application/services/strategic-report-analytics.service.spec.ts new file mode 100644 index 00000000..3fa355f9 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/services/strategic-report-analytics.service.spec.ts @@ -0,0 +1,95 @@ +import { NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { STRATEGIC_REPORT_ANALYTICS_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report-analytics.repository.port'; +import { StrategicReportAnalyticsService } from 'src/contexts/reporting/application/services/strategic-report-analytics.service'; +import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service'; +import { StrategicReportAnalyticsDomain } from 'src/contexts/reporting/domain/strategic-report-analytics.domain'; + +function buildAnalytics( + strategicReportId = 10, +): StrategicReportAnalyticsDomain { + return StrategicReportAnalyticsDomain.create({ + id: 1, + strategicReportId, + competenceId: 7, + competenceTitle: 'Teamwork', + }); +} + +describe('StrategicReportAnalyticsService', () => { + let service: StrategicReportAnalyticsService; + let analytics: any; + let strategicReporting: any; + + beforeEach(async () => { + analytics = { + findByStrategicReportId: jest.fn(), + findById: jest.fn(), + }; + strategicReporting = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + StrategicReportAnalyticsService, + { + provide: STRATEGIC_REPORT_ANALYTICS_REPOSITORY, + useValue: analytics, + }, + { + provide: StrategicReportingService, + useValue: strategicReporting, + }, + ], + }).compile(); + + service = module.get(StrategicReportAnalyticsService); + }); + + describe('getByStrategicReportId', () => { + it('returns analytics after the access check passes', async () => { + strategicReporting.getById.mockResolvedValue({ id: 10 }); + const rows = [buildAnalytics()]; + analytics.findByStrategicReportId.mockResolvedValue(rows); + + await expect(service.getByStrategicReportId(10)).resolves.toBe( + rows, + ); + expect(strategicReporting.getById).toHaveBeenCalledWith( + 10, + undefined, + ); + }); + + it('throws NotFoundException when the repo returns null', async () => { + strategicReporting.getById.mockResolvedValue({ id: 10 }); + analytics.findByStrategicReportId.mockResolvedValue(null); + + await expect( + service.getByStrategicReportId(10), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('getById', () => { + it('throws NotFoundException when missing', async () => { + analytics.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('re-checks access via the parent strategic report', async () => { + const row = buildAnalytics(99); + analytics.findById.mockResolvedValue(row); + strategicReporting.getById.mockResolvedValue({ id: 99 }); + + const result = await service.getById(1); + + expect(strategicReporting.getById).toHaveBeenCalledWith( + 99, + undefined, + ); + expect(result).toBe(row); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/services/strategic-report-insight.service.spec.ts b/apps/api/test/unit/contexts/reporting/application/services/strategic-report-insight.service.spec.ts new file mode 100644 index 00000000..05d2c731 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/services/strategic-report-insight.service.spec.ts @@ -0,0 +1,91 @@ +import { InsightType } from '@intra/shared-kernel'; +import { NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { STRATEGIC_REPORT_INSIGHT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report-insight.repository.port'; +import { StartegicReportInsightService } from 'src/contexts/reporting/application/services/startegic-report-insight.service'; +import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service'; +import { StrategicReportInsightDomain } from 'src/contexts/reporting/domain/startegic-report-insight.domain'; + +function buildInsight(strategicReportId = 10): StrategicReportInsightDomain { + return StrategicReportInsightDomain.create({ + id: 1, + strategicReportId, + insightType: InsightType.HIGHEST_RATING, + competenceId: 7, + competenceTitle: 'Teamwork', + }); +} + +describe('StartegicReportInsightService', () => { + let service: StartegicReportInsightService; + let insights: any; + let strategicReporting: any; + + beforeEach(async () => { + insights = { + findByStrategicReportId: jest.fn(), + findById: jest.fn(), + }; + strategicReporting = { getById: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + StartegicReportInsightService, + { + provide: STRATEGIC_REPORT_INSIGHT_REPOSITORY, + useValue: insights, + }, + { + provide: StrategicReportingService, + useValue: strategicReporting, + }, + ], + }).compile(); + + service = module.get(StartegicReportInsightService); + }); + + describe('getByStrategicReportId', () => { + it('returns insights after the access check passes', async () => { + strategicReporting.getById.mockResolvedValue({ id: 10 }); + const rows = [buildInsight()]; + insights.findByStrategicReportId.mockResolvedValue(rows); + + await expect(service.getByStrategicReportId(10)).resolves.toBe( + rows, + ); + }); + + it('throws NotFoundException when the repo returns null', async () => { + strategicReporting.getById.mockResolvedValue({ id: 10 }); + insights.findByStrategicReportId.mockResolvedValue(null); + + await expect( + service.getByStrategicReportId(10), + ).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('getById', () => { + it('throws NotFoundException when missing', async () => { + insights.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('re-checks access via the parent strategic report', async () => { + const row = buildInsight(50); + insights.findById.mockResolvedValue(row); + strategicReporting.getById.mockResolvedValue({ id: 50 }); + + const result = await service.getById(1); + + expect(strategicReporting.getById).toHaveBeenCalledWith( + 50, + undefined, + ); + expect(result).toBe(row); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/services/strategic-reports.service.spec.ts b/apps/api/test/unit/contexts/reporting/application/services/strategic-reports.service.spec.ts new file mode 100644 index 00000000..f67f791e --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/services/strategic-reports.service.spec.ts @@ -0,0 +1,621 @@ +import { + AnswerType, + IdentityRole, + IdentityStatus, + RespondentCategory, +} from '@intra/shared-kernel'; +import { ForbiddenException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ANSWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/answer.repository.port'; +import { CYCLE_REPOSITORY } from 'src/contexts/feedback360/application/ports/cycle.repository.port'; +import { QUESTION_REPOSITORY } from 'src/contexts/feedback360/application/ports/question.repository.port'; +import { RESPONDENT_REPOSITORY } from 'src/contexts/feedback360/application/ports/respondent.repository.port'; +import { REVIEW_REPOSITORY } from 'src/contexts/feedback360/application/ports/review.repository.port'; +import { REVIEWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/reviewer.repository.port'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain'; +import { RespondentDomain } from 'src/contexts/feedback360/domain/respondent.domain'; +import { ReviewDomain } from 'src/contexts/feedback360/domain/review.domain'; +import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain'; +import { IDENTITY_USER_REPOSITORY } from 'src/contexts/identity/application/ports/user.repository.port'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { COMPETENCE_REPOSITORY } from 'src/contexts/library/application/ports/competence.repository.port'; +import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain'; +import { ORGANISATION_POSITION_REPOSITORY } from 'src/contexts/organisation/application/ports/position.repository.port'; +import { ORGANISATION_TEAM_REPOSITORY } from 'src/contexts/organisation/application/ports/team.repository.port'; +import { PositionDomain } from 'src/contexts/organisation/domain/position.domain'; +import { TeamDomain } from 'src/contexts/organisation/domain/team.domain'; +import { REPORT_ANALYTICS_REPOSITORY } from 'src/contexts/reporting/application/ports/report-analytics.repository.port'; +import { REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/report.repository.port'; +import { STRATEGIC_REPORT_ANALYTICS_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report-analytics.repository.port'; +import { STRATEGIC_REPORT_INSIGHT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report-insight.repository.port'; +import { STRATEGIC_REPORT_REPOSITORY } from 'src/contexts/reporting/application/ports/strategic-report.repository.port'; +import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; + +function buildUser( + overrides: Partial[0]> = {}, +): UserDomain { + return UserDomain.create({ + id: 5, + firstName: 'A', + lastName: 'B', + email: 'a@b.com', + roles: [IdentityRole.EMPLOYEE], + ...overrides, + }); +} + +function buildStrategicReport(cycleId = 100): StrategicReportDomain { + return StrategicReportDomain.create({ + id: 1, + cycleId, + cycleTitle: 'Q1', + rateeCount: 0, + rateeIds: [], + respondentCount: 0, + respondentIds: [], + answerCount: 0, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 0, + competenceIds: [], + questionCount: 0, + questionIds: [], + }); +} + +function buildReview( + overrides: Partial[0]> = {}, +): ReviewDomain { + return ReviewDomain.create({ + id: 10, + rateeId: 1, + rateeFullName: 'R', + rateePositionId: 1, + rateePositionTitle: 'Eng', + hrId: 2, + hrFullName: 'HR', + cycleId: 100, + ...overrides, + }); +} + +describe('StrategicReportingService', () => { + let service: StrategicReportingService; + let strategicReports: any; + let strategicAnalyticsRepo: any; + let respondents: any; + let reviewers: any; + let answers: any; + let reviews: any; + let cyclesRepo: any; + let reports: any; + let users: any; + let teams: any; + let positions: any; + let competences: any; + let questions: any; + let reportAnalyticsRepo: any; + let insightsRepo: any; + + beforeEach(async () => { + strategicReports = { + search: jest.fn(), + findById: jest.fn(), + findByCycleId: jest.fn(), + create: jest.fn(), + }; + strategicAnalyticsRepo = { createMany: jest.fn() }; + respondents = { listByReview: jest.fn() }; + reviewers = { listByReview: jest.fn() }; + answers = { + list: jest.fn(), + getAnswersCountByRespondentCategories: jest.fn(), + }; + reviews = { findById: jest.fn(), search: jest.fn() }; + cyclesRepo = { findById: jest.fn() }; + reports = { search: jest.fn() }; + users = { findById: jest.fn() }; + teams = { findById: jest.fn() }; + positions = { findById: jest.fn() }; + competences = { findById: jest.fn() }; + questions = { findById: jest.fn() }; + reportAnalyticsRepo = { findByReportId: jest.fn() }; + insightsRepo = { createMany: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + StrategicReportingService, + { + provide: STRATEGIC_REPORT_REPOSITORY, + useValue: strategicReports, + }, + { + provide: STRATEGIC_REPORT_ANALYTICS_REPOSITORY, + useValue: strategicAnalyticsRepo, + }, + { provide: RESPONDENT_REPOSITORY, useValue: respondents }, + { provide: REVIEWER_REPOSITORY, useValue: reviewers }, + { provide: ANSWER_REPOSITORY, useValue: answers }, + { provide: REVIEW_REPOSITORY, useValue: reviews }, + { provide: CYCLE_REPOSITORY, useValue: cyclesRepo }, + { provide: REPORT_REPOSITORY, useValue: reports }, + { provide: IDENTITY_USER_REPOSITORY, useValue: users }, + { provide: ORGANISATION_TEAM_REPOSITORY, useValue: teams }, + { + provide: ORGANISATION_POSITION_REPOSITORY, + useValue: positions, + }, + { provide: COMPETENCE_REPOSITORY, useValue: competences }, + { provide: QUESTION_REPOSITORY, useValue: questions }, + { + provide: REPORT_ANALYTICS_REPOSITORY, + useValue: reportAnalyticsRepo, + }, + { + provide: STRATEGIC_REPORT_INSIGHT_REPOSITORY, + useValue: insightsRepo, + }, + ], + }).compile(); + + service = module.get(StrategicReportingService); + }); + + describe('search', () => { + it('delegates to the repository when no actor is supplied', async () => { + strategicReports.search.mockResolvedValue([buildStrategicReport()]); + const result = await service.search({}); + expect(result).toHaveLength(1); + }); + + it('throws ForbiddenException for non-admin/HR actors', async () => { + await expect( + service.search( + {}, + buildUser({ roles: [IdentityRole.EMPLOYEE] }), + ), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + + it('returns reports for admin/HR actors', async () => { + strategicReports.search.mockResolvedValue([]); + await expect( + service.search({}, buildUser({ roles: [IdentityRole.HR] })), + ).resolves.toEqual([]); + }); + }); + + describe('getById', () => { + it('throws NotFoundException when the report is missing', async () => { + strategicReports.findById.mockResolvedValue(null); + await expect(service.getById(1)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('returns the report for HR actors', async () => { + const report = buildStrategicReport(); + strategicReports.findById.mockResolvedValue(report); + + const result = await service.getById( + 1, + buildUser({ roles: [IdentityRole.HR] }), + ); + expect(result).toBe(report); + }); + + it('throws NotFoundException when the cycle has no reviews', async () => { + strategicReports.findById.mockResolvedValue(buildStrategicReport()); + reviews.search.mockResolvedValue([]); + + await expect( + service.getById( + 1, + buildUser({ id: 999, roles: [IdentityRole.EMPLOYEE] }), + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('returns the report when actor manages at least one review in the cycle', async () => { + const report = buildStrategicReport(); + strategicReports.findById.mockResolvedValue(report); + reviews.search.mockResolvedValue([buildReview({ managerId: 5 })]); + + const result = await service.getById( + 1, + buildUser({ id: 5, roles: [IdentityRole.MANAGER] }), + ); + expect(result).toBe(report); + }); + + it('throws ForbiddenException when actor has no relation to the cycle reviews', async () => { + strategicReports.findById.mockResolvedValue(buildStrategicReport()); + reviews.search.mockResolvedValue([buildReview({ managerId: 99 })]); + + await expect( + service.getById( + 1, + buildUser({ id: 5, roles: [IdentityRole.EMPLOYEE] }), + ), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + }); + + describe('getByCycleId', () => { + it('throws NotFoundException when no report exists for the cycle', async () => { + strategicReports.findByCycleId.mockResolvedValue(null); + await expect(service.getByCycleId(100)).rejects.toBeInstanceOf( + NotFoundException, + ); + }); + + it('returns the report after checking access', async () => { + const report = buildStrategicReport(); + strategicReports.findByCycleId.mockResolvedValue(report); + + const result = await service.getByCycleId(100); + expect(result).toBe(report); + }); + }); + + describe('generateStrategicReportForCycle', () => { + it('throws NotFoundException when the cycle does not exist', async () => { + cyclesRepo.findById.mockResolvedValue(null); + await expect( + service.generateStrategicReportForCycle(100), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('returns the existing strategic report when one already exists', async () => { + cyclesRepo.findById.mockResolvedValue({ id: 100, title: 'Q1' }); + const existing = buildStrategicReport(); + strategicReports.findByCycleId.mockResolvedValue(existing); + + const result = await service.generateStrategicReportForCycle(100); + + expect(result).toBe(existing); + expect(strategicReports.create).not.toHaveBeenCalled(); + }); + + it('throws NotFoundException when there are no per-review reports', async () => { + cyclesRepo.findById.mockResolvedValue({ id: 100, title: 'Q1' }); + strategicReports.findByCycleId.mockResolvedValue(null); + reports.search.mockResolvedValue([]); + + await expect( + service.generateStrategicReportForCycle(100), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('throws when the freshly created report cannot be re-loaded', async () => { + cyclesRepo.findById.mockResolvedValue({ id: 100, title: 'Q1' }); + strategicReports.findByCycleId.mockResolvedValue(null); + reports.search.mockResolvedValue([ + ReportDomain.create({ + id: 1, + reviewId: 10, + cycleId: 100, + respondentCount: 1, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 1, + analytics: [], + }), + ]); + reviews.findById.mockResolvedValue(null); + reportAnalyticsRepo.findByReportId.mockResolvedValue([]); + strategicReports.create.mockResolvedValue( + StrategicReportDomain.create({ + id: 42, + cycleId: 100, + cycleTitle: 'Q1', + rateeCount: 0, + rateeIds: [], + respondentCount: 0, + respondentIds: [], + answerCount: 0, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 0, + competenceIds: [], + questionCount: 0, + questionIds: [], + }), + ); + strategicReports.findById.mockResolvedValue(null); + + await expect( + service.generateStrategicReportForCycle(100), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('aggregates reviews, ratees, respondents, answers, reviewers, and competences into the report', async () => { + cyclesRepo.findById.mockResolvedValue({ + id: 100, + title: 'Q1 2025', + }); + strategicReports.findByCycleId.mockResolvedValue(null); + + const perReviewReports = [ + ReportDomain.create({ + id: 1, + reviewId: 10, + cycleId: 100, + respondentCount: 2, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 4, + turnoutPctOfTeam: 80, + turnoutPctOfOther: 50, + analytics: [], + }), + ]; + reports.search.mockResolvedValue(perReviewReports); + + const review = ReviewDomain.create({ + id: 10, + rateeId: 11, + rateeFullName: 'Jane Doe', + rateePositionId: 3, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + cycleId: 100, + reportId: 1, + }); + reviews.findById.mockResolvedValue(review); + + users.findById.mockImplementation(async (id: number) => + UserDomain.create({ + id, + firstName: id === 11 ? 'Jane' : 'X', + lastName: 'Doe', + email: `u${id}@example.com`, + status: IdentityStatus.ACTIVE, + positionId: 3, + teamId: 4, + roles: [IdentityRole.EMPLOYEE], + }), + ); + respondents.listByReview.mockResolvedValue([ + RespondentDomain.create({ + reviewId: 10, + respondentId: 20, + fullName: 'A B', + category: RespondentCategory.TEAM, + positionId: 3, + positionTitle: 'Engineer', + teamId: 4, + }), + RespondentDomain.create({ + reviewId: 10, + respondentId: 21, + fullName: 'C D', + category: RespondentCategory.OTHER, + positionId: 3, + positionTitle: 'Engineer', + teamId: 5, + }), + ]); + teams.findById.mockImplementation(async (id: number) => + TeamDomain.create({ id, title: `Team ${id}` }), + ); + positions.findById.mockImplementation(async (id: number) => + PositionDomain.create({ id, title: `Position ${id}` }), + ); + answers.list.mockResolvedValue([ + AnswerDomain.create({ + reviewId: 10, + questionId: 100, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 4, + }), + AnswerDomain.create({ + reviewId: 10, + questionId: 100, + respondentCategory: RespondentCategory.SELF_ASSESSMENT, + answerType: AnswerType.NUMERICAL_SCALE, + numericalValue: 5, + }), + ]); + answers.getAnswersCountByRespondentCategories.mockResolvedValue([ + { + respondentCategory: RespondentCategory.SELF_ASSESSMENT, + answers: 1, + }, + { + respondentCategory: RespondentCategory.TEAM, + answers: 3, + }, + { + respondentCategory: RespondentCategory.OTHER, + answers: 0, + }, + ]); + questions.findById.mockImplementation(async (id: number) => + QuestionDomain.create({ + id, + title: `Q ${id}`, + answerType: AnswerType.NUMERICAL_SCALE, + competenceId: 7, + }), + ); + competences.findById.mockResolvedValue( + CompetenceDomain.create({ + id: 7, + title: 'Teamwork', + }), + ); + reviewers.listByReview.mockResolvedValue([ + ReviewerDomain.create({ + reviewId: 10, + reviewerId: 40, + fullName: 'Rev One', + positionId: 3, + positionTitle: 'Senior Engineer', + }), + ]); + reportAnalyticsRepo.findByReportId.mockResolvedValue([ + ReportAnalyticsDomain.create({ + reportId: 1, + entityType: 'COMPETENCE' as any, + questionId: null, + questionTitle: null, + competenceId: 7, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4, + averageByTeam: 3.5, + averageByOther: 4, + percentageBySelfAssessment: 80, + percentageByTeam: 70, + percentageByOther: 80, + deltaPercentageByTeam: -10, + deltaPercentageByOther: 0, + }), + ]); + strategicReports.create.mockImplementation( + async (r: StrategicReportDomain) => + StrategicReportDomain.create({ ...r, id: 42 }), + ); + strategicReports.findById.mockImplementation(async (id: number) => + StrategicReportDomain.create({ + id, + cycleId: 100, + cycleTitle: 'Q1 2025', + rateeCount: 1, + rateeIds: [11], + respondentCount: 2, + respondentIds: [20, 21], + answerCount: 4, + reviewerCount: 1, + reviewerIds: [40], + teamCount: 2, + teamIds: [4, 5], + positionCount: 1, + positionIds: [3], + competenceCount: 1, + competenceIds: [7], + questionCount: 1, + questionIds: [100], + }), + ); + + const result = await service.generateStrategicReportForCycle(100); + + expect(result.id).toBe(42); + // The aggregated report was persisted with the collected IDs + expect(strategicReports.create).toHaveBeenCalledTimes(1); + const created = strategicReports.create.mock + .calls[0][0] as StrategicReportDomain; + expect(created.cycleTitle).toBe('Q1 2025'); + expect(created.rateeIds).toEqual([11]); + expect(created.respondentIds).toEqual([20, 21]); + expect(created.reviewerIds).toEqual([40]); + expect(created.teamIds).toEqual([4, 5]); + expect(created.positionIds).toEqual([3]); + expect(created.competenceIds).toEqual([7]); + expect(created.questionIds).toEqual([100]); + expect(created.answerCount).toBe(4); + // SELF_ASSESSMENT > 0 → rateeTurnout is 100 + expect(created.turnoutAvgPctOfRatees!.toNumber()).toBe(100); + + // Strategic competence analytics were persisted + expect(strategicAnalyticsRepo.createMany).toHaveBeenCalledTimes(1); + const passedAnalytics = + strategicAnalyticsRepo.createMany.mock.calls[0][1]; + expect(passedAnalytics).toHaveLength(1); + expect(passedAnalytics[0].competenceId).toBe(7); + + // Four insight kinds (HIGHEST/LOWEST rating + delta) were persisted + expect(insightsRepo.createMany).toHaveBeenCalledTimes(1); + const passedInsights = insightsRepo.createMany.mock.calls[0][1]; + expect(passedInsights).toHaveLength(4); + }); + + it('keeps rateeTurnout at 0 when no self-assessment answer is found for the review', async () => { + cyclesRepo.findById.mockResolvedValue({ id: 100, title: 'Q1' }); + strategicReports.findByCycleId.mockResolvedValue(null); + reports.search.mockResolvedValue([ + ReportDomain.create({ + id: 1, + reviewId: 10, + cycleId: 100, + respondentCount: 0, + respondentCategories: [], + answerCount: 0, + analytics: [], + }), + ]); + reviews.findById.mockResolvedValue( + ReviewDomain.create({ + id: 10, + rateeId: 11, + rateeFullName: 'Jane Doe', + rateePositionId: 3, + rateePositionTitle: 'Engineer', + hrId: 2, + hrFullName: 'HR', + cycleId: 100, + reportId: 1, + }), + ); + users.findById.mockResolvedValue( + UserDomain.create({ + id: 11, + firstName: 'Jane', + lastName: 'Doe', + email: 'j@example.com', + }), + ); + respondents.listByReview.mockResolvedValue([]); + answers.list.mockResolvedValue([]); + answers.getAnswersCountByRespondentCategories.mockResolvedValue([]); + reviewers.listByReview.mockResolvedValue([]); + reportAnalyticsRepo.findByReportId.mockResolvedValue([]); + strategicReports.create.mockImplementation( + async (r: StrategicReportDomain) => + StrategicReportDomain.create({ ...r, id: 50 }), + ); + strategicReports.findById.mockResolvedValue( + StrategicReportDomain.create({ + id: 50, + cycleId: 100, + cycleTitle: 'Q1', + rateeCount: 1, + rateeIds: [11], + respondentCount: 0, + respondentIds: [], + answerCount: 0, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 0, + competenceIds: [], + questionCount: 0, + questionIds: [], + }), + ); + + const result = await service.generateStrategicReportForCycle(100); + + const created = strategicReports.create.mock + .calls[0][0] as StrategicReportDomain; + expect(created.turnoutAvgPctOfRatees!.toNumber()).toBe(0); + expect(result.id).toBe(50); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/application/services/text-answer.service.spec.ts b/apps/api/test/unit/contexts/reporting/application/services/text-answer.service.spec.ts new file mode 100644 index 00000000..b0d291d8 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/application/services/text-answer.service.spec.ts @@ -0,0 +1,149 @@ +import { + AnswerType, + IdentityRole, + RespondentCategory, +} from '@intra/shared-kernel'; +import { ForbiddenException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ANSWER_REPOSITORY } from 'src/contexts/feedback360/application/ports/answer.repository.port'; +import { REVIEW_QUESTION_RELATION_REPOSITORY } from 'src/contexts/feedback360/application/ports/review-question-relation.repository.port'; +import { AnswerDomain } from 'src/contexts/feedback360/domain/answer.domain'; +import { ReviewQuestionRelationDomain } from 'src/contexts/feedback360/domain/review-question-relation.domain'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { TextAnswerService } from 'src/contexts/reporting/application/services/text-answer.service'; + +describe('TextAnswerService', () => { + let service: TextAnswerService; + let answers: any; + let relations: any; + + beforeEach(async () => { + answers = { list: jest.fn() }; + relations = { listByReview: jest.fn() }; + + const module = await Test.createTestingModule({ + providers: [ + TextAnswerService, + { provide: ANSWER_REPOSITORY, useValue: answers }, + { + provide: REVIEW_QUESTION_RELATION_REPOSITORY, + useValue: relations, + }, + ], + }).compile(); + + service = module.get(TextAnswerService); + }); + + describe('listByReview', () => { + it('returns text answers paired with their question titles for admin/HR actors', async () => { + const actor = UserDomain.create({ + id: 1, + firstName: 'A', + lastName: 'B', + email: 'a@b.com', + roles: [IdentityRole.HR], + }); + + answers.list.mockResolvedValue([ + AnswerDomain.create({ + reviewId: 10, + questionId: 100, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.TEXT_FIELD, + textValue: 'Great work', + }), + ]); + relations.listByReview.mockResolvedValue([ + ReviewQuestionRelationDomain.create({ + reviewId: 10, + questionId: 100, + questionTitle: 'Q?', + answerType: AnswerType.TEXT_FIELD, + competenceId: 7, + competenceTitle: 'Teamwork', + }), + ]); + + const result = await service.listByReview(10, actor); + + expect(answers.list).toHaveBeenCalledWith({ + reviewId: 10, + answerType: AnswerType.TEXT_FIELD, + }); + expect(result).toEqual([ + { + questionId: 100, + questionTitle: 'Q?', + respondentCategory: RespondentCategory.TEAM, + textValue: 'Great work', + }, + ]); + }); + + it('skips answers with empty or whitespace-only text', async () => { + const actor = UserDomain.create({ + id: 1, + firstName: 'A', + lastName: 'B', + email: 'a@b.com', + roles: [IdentityRole.ADMIN], + }); + + answers.list.mockResolvedValue([ + AnswerDomain.create({ + reviewId: 10, + questionId: 100, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.TEXT_FIELD, + textValue: ' ', + }), + AnswerDomain.create({ + reviewId: 10, + questionId: 101, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.TEXT_FIELD, + textValue: '', + }), + AnswerDomain.create({ + reviewId: 10, + questionId: 102, + respondentCategory: RespondentCategory.TEAM, + answerType: AnswerType.TEXT_FIELD, + textValue: 'real', + }), + ]); + relations.listByReview.mockResolvedValue([]); + + const result = await service.listByReview(10, actor); + + expect(result).toHaveLength(1); + expect(result[0].textValue).toBe('real'); + expect(result[0].questionTitle).toBeNull(); + }); + + it('throws ForbiddenException for non-admin/HR actors', async () => { + const actor = UserDomain.create({ + id: 1, + firstName: 'A', + lastName: 'B', + email: 'a@b.com', + roles: [IdentityRole.EMPLOYEE], + }); + + await expect( + service.listByReview(10, actor), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + + it('skips the access check when no actor is supplied', async () => { + answers.list.mockResolvedValue([]); + relations.listByReview.mockResolvedValue([]); + + const result = await service.listByReview(10); + + expect(result).toEqual([]); + expect(answers.list).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/domain/report-analytics.domain.spec.ts b/apps/api/test/unit/contexts/reporting/domain/report-analytics.domain.spec.ts new file mode 100644 index 00000000..8be16e0d --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/domain/report-analytics.domain.spec.ts @@ -0,0 +1,70 @@ +import { EntityType } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; + +describe('ReportAnalyticsDomain', () => { + const baseProps = { + reportId: 1, + entityType: EntityType.QUESTION, + questionId: 10, + questionTitle: 'How effective?', + competenceId: null, + competenceTitle: null, + }; + + describe('create', () => { + it('creates a question analytics row with every decimal coerced', () => { + const analytics = ReportAnalyticsDomain.create({ + id: 100, + ...baseProps, + averageBySelfAssessment: 4, + averageByTeam: 3.5, + averageByOther: '4.25', + percentageBySelfAssessment: 80, + percentageByTeam: 70, + percentageByOther: 85, + deltaPercentageByTeam: -10, + deltaPercentageByOther: 5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(analytics.id).toBe(100); + expect(analytics.reportId).toBe(1); + expect(analytics.entityType).toBe(EntityType.QUESTION); + expect(analytics.questionId).toBe(10); + expect(analytics.averageByOther).toBeInstanceOf(Decimal); + expect(analytics.averageByOther!.toString()).toBe('4.25'); + expect(analytics.deltaPercentageByTeam!.toNumber()).toBe(-10); + }); + + it('creates a competence analytics row, normalising missing question fields to null', () => { + const analytics = ReportAnalyticsDomain.create({ + reportId: 2, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: 5, + competenceTitle: 'Leadership', + }); + + expect(analytics.entityType).toBe(EntityType.COMPETENCE); + expect(analytics.competenceId).toBe(5); + expect(analytics.competenceTitle).toBe('Leadership'); + expect(analytics.questionId).toBeNull(); + expect(analytics.questionTitle).toBeNull(); + }); + + it('defaults all decimal fields to null when missing', () => { + const analytics = ReportAnalyticsDomain.create(baseProps); + + expect(analytics.averageBySelfAssessment).toBeNull(); + expect(analytics.averageByTeam).toBeNull(); + expect(analytics.averageByOther).toBeNull(); + expect(analytics.percentageBySelfAssessment).toBeNull(); + expect(analytics.percentageByTeam).toBeNull(); + expect(analytics.percentageByOther).toBeNull(); + expect(analytics.deltaPercentageByTeam).toBeNull(); + expect(analytics.deltaPercentageByOther).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/domain/report-comment.domain.spec.ts b/apps/api/test/unit/contexts/reporting/domain/report-comment.domain.spec.ts new file mode 100644 index 00000000..2a1e92c5 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/domain/report-comment.domain.spec.ts @@ -0,0 +1,52 @@ +import { CommentSentiment, RespondentCategory } from '@intra/shared-kernel'; +import { ReportCommentDomain } from 'src/contexts/reporting/domain/report-comment.domain'; + +describe('ReportCommentDomain', () => { + const baseProps = { + reportId: 1, + questionId: 2, + questionTitle: 'How is teamwork?', + comment: 'Great collaboration', + respondentCategories: [RespondentCategory.TEAM], + numberOfMentions: 3, + }; + + describe('create', () => { + it('creates a comment with all supplied fields', () => { + const comment = ReportCommentDomain.create({ + id: 10, + ...baseProps, + commentSentiment: CommentSentiment.POSITIVE, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(comment.id).toBe(10); + expect(comment.reportId).toBe(1); + expect(comment.questionId).toBe(2); + expect(comment.questionTitle).toBe('How is teamwork?'); + expect(comment.comment).toBe('Great collaboration'); + expect(comment.respondentCategories).toEqual([ + RespondentCategory.TEAM, + ]); + expect(comment.commentSentiment).toBe(CommentSentiment.POSITIVE); + expect(comment.numberOfMentions).toBe(3); + expect(comment.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('defaults respondentCategories to an empty array when null is passed', () => { + const comment = ReportCommentDomain.create({ + ...baseProps, + respondentCategories: null as any, + }); + + expect(comment.respondentCategories).toEqual([]); + }); + + it('defaults sentiment to null when missing', () => { + const comment = ReportCommentDomain.create(baseProps); + expect(comment.commentSentiment).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/domain/report-insight.domain.spec.ts b/apps/api/test/unit/contexts/reporting/domain/report-insight.domain.spec.ts new file mode 100644 index 00000000..6416f037 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/domain/report-insight.domain.spec.ts @@ -0,0 +1,55 @@ +import { EntityType, InsightType } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { ReportInsightDomain } from 'src/contexts/reporting/domain/report-insight.domain'; + +describe('ReportInsightDomain', () => { + const baseProps = { + reportId: 1, + insightType: InsightType.HIGHEST_RATING, + entityType: EntityType.QUESTION, + questionId: 10, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + }; + + describe('create', () => { + it('creates a question insight with all decimal fields coerced', () => { + const insight = ReportInsightDomain.create({ + id: 5, + ...baseProps, + averageScore: '4.5', + averageRating: 90, + averageDelta: -5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(insight.id).toBe(5); + expect(insight.insightType).toBe(InsightType.HIGHEST_RATING); + expect(insight.entityType).toBe(EntityType.QUESTION); + expect(insight.questionId).toBe(10); + expect(insight.averageScore).toBeInstanceOf(Decimal); + expect(insight.averageScore!.toString()).toBe('4.5'); + expect(insight.averageRating!.toNumber()).toBe(90); + expect(insight.averageDelta!.toNumber()).toBe(-5); + }); + + it('defaults missing decimal/optional fields to null', () => { + const insight = ReportInsightDomain.create(baseProps); + + expect(insight.averageScore).toBeNull(); + expect(insight.averageRating).toBeNull(); + expect(insight.averageDelta).toBeNull(); + }); + + it('normalises competence fields to null for question insights', () => { + const insight = ReportInsightDomain.create({ + ...baseProps, + competenceId: null, + competenceTitle: null, + }); + expect(insight.competenceId).toBeNull(); + expect(insight.competenceTitle).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/domain/report.domain.spec.ts b/apps/api/test/unit/contexts/reporting/domain/report.domain.spec.ts new file mode 100644 index 00000000..69ecea7f --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/domain/report.domain.spec.ts @@ -0,0 +1,118 @@ +import { RespondentCategory } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; + +describe('ReportDomain', () => { + const baseProps = { + reviewId: 1, + respondentCount: 5, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 20, + analytics: [], + }; + + describe('create', () => { + it('creates a report with every supplied numeric field as Decimal', () => { + const report = ReportDomain.create({ + id: 10, + ...baseProps, + cycleId: 100, + turnoutPctOfTeam: '75.5', + turnoutPctOfOther: 80, + questionTotAvgBySelf: 4, + questionTotAvgByTeam: 3.5, + questionTotAvgByOthers: 4.2, + questionTotPctBySelf: 80, + questionTotPctByTeam: 70, + questionTotPctByOthers: 84, + questionTotDeltaPctByTeam: -10, + questionTotDeltaPctByOthers: 4, + competenceTotAvgBySelf: 4.5, + competenceTotAvgByTeam: 3.8, + competenceTotAvgByOthers: 4.1, + competenceTotPctBySelf: 90, + competenceTotPctByTeam: 76, + competenceTotPctByOthers: 82, + competenceTotDeltaPctByTeam: -14, + competenceTotDeltaPctByOthers: -8, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + comments: [], + insights: [], + }); + + expect(report.id).toBe(10); + expect(report.reviewId).toBe(1); + expect(report.cycleId).toBe(100); + expect(report.respondentCount).toBe(5); + expect(report.respondentCategories).toEqual([ + RespondentCategory.TEAM, + ]); + expect(report.answerCount).toBe(20); + expect(report.turnoutPctOfTeam).toBeInstanceOf(Decimal); + expect(report.turnoutPctOfTeam!.toString()).toBe('75.5'); + expect(report.turnoutPctOfOther).toBeInstanceOf(Decimal); + expect(report.turnoutPctOfOther!.toNumber()).toBe(80); + expect(report.questionTotAvgBySelf!.toNumber()).toBe(4); + expect(report.competenceTotDeltaPctByOthers!.toNumber()).toBe(-8); + expect(report.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('defaults cycleId and missing decimal fields to null', () => { + const report = ReportDomain.create(baseProps); + + expect(report.cycleId).toBeNull(); + expect(report.turnoutPctOfTeam).toBeNull(); + expect(report.turnoutPctOfOther).toBeNull(); + expect(report.questionTotAvgBySelf).toBeNull(); + expect(report.competenceTotDeltaPctByOthers).toBeNull(); + }); + + it('defaults analytics, comments, and insights to empty arrays', () => { + const report = ReportDomain.create(baseProps); + + expect(report.analytics).toEqual([]); + expect(report.comments).toEqual([]); + expect(report.insights).toEqual([]); + }); + + it('preserves explicit null on optional decimal fields', () => { + const report = ReportDomain.create({ + ...baseProps, + turnoutPctOfTeam: null, + turnoutPctOfOther: null, + }); + + expect(report.turnoutPctOfTeam).toBeNull(); + expect(report.turnoutPctOfOther).toBeNull(); + }); + }); + + describe('withAnalytics / withComments / withInsights', () => { + it('returns a new instance carrying the supplied analytics', () => { + const report = ReportDomain.create(baseProps); + const next = report.withAnalytics([{ id: 1 } as any]); + + expect(next).not.toBe(report); + expect(next.analytics).toEqual([{ id: 1 }]); + expect(report.analytics).toEqual([]); + }); + + it('returns a new instance carrying the supplied comments', () => { + const report = ReportDomain.create(baseProps); + const next = report.withComments([{ id: 2 } as any]); + + expect(next).not.toBe(report); + expect(next.comments).toEqual([{ id: 2 }]); + }); + + it('returns a new instance carrying the supplied insights', () => { + const report = ReportDomain.create(baseProps); + const next = report.withInsights([{ id: 3 } as any]); + + expect(next).not.toBe(report); + expect(next.insights).toEqual([{ id: 3 }]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/domain/strategic-report-analytics.domain.spec.ts b/apps/api/test/unit/contexts/reporting/domain/strategic-report-analytics.domain.spec.ts new file mode 100644 index 00000000..06b1359b --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/domain/strategic-report-analytics.domain.spec.ts @@ -0,0 +1,50 @@ +import Decimal from 'decimal.js'; +import { StrategicReportAnalyticsDomain } from 'src/contexts/reporting/domain/strategic-report-analytics.domain'; + +describe('StrategicReportAnalyticsDomain', () => { + const baseProps = { + strategicReportId: 1, + competenceId: 7, + competenceTitle: 'Teamwork', + }; + + describe('create', () => { + it('creates analytics with every decimal field coerced', () => { + const analytics = StrategicReportAnalyticsDomain.create({ + id: 100, + ...baseProps, + averageBySelfAssessment: 4, + averageByTeam: 3.5, + averageByOther: '3.9', + percentageBySelfAssessment: 80, + percentageByTeam: 70, + percentageByOther: 78, + deltaPercentageByTeam: -10, + deltaPercentageByOther: -2, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(analytics.id).toBe(100); + expect(analytics.strategicReportId).toBe(1); + expect(analytics.competenceId).toBe(7); + expect(analytics.competenceTitle).toBe('Teamwork'); + expect(analytics.averageByOther).toBeInstanceOf(Decimal); + expect(analytics.averageByOther!.toString()).toBe('3.9'); + expect(analytics.deltaPercentageByTeam!.toNumber()).toBe(-10); + expect(analytics.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('defaults missing decimal fields to null', () => { + const analytics = StrategicReportAnalyticsDomain.create(baseProps); + + expect(analytics.averageBySelfAssessment).toBeNull(); + expect(analytics.averageByTeam).toBeNull(); + expect(analytics.averageByOther).toBeNull(); + expect(analytics.percentageBySelfAssessment).toBeNull(); + expect(analytics.deltaPercentageByTeam).toBeNull(); + expect(analytics.deltaPercentageByOther).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/domain/strategic-report-insight.domain.spec.ts b/apps/api/test/unit/contexts/reporting/domain/strategic-report-insight.domain.spec.ts new file mode 100644 index 00000000..427c8727 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/domain/strategic-report-insight.domain.spec.ts @@ -0,0 +1,43 @@ +import { InsightType } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { StrategicReportInsightDomain } from 'src/contexts/reporting/domain/startegic-report-insight.domain'; + +describe('StrategicReportInsightDomain', () => { + const baseProps = { + strategicReportId: 1, + insightType: InsightType.HIGHEST_RATING, + competenceId: 7, + competenceTitle: 'Teamwork', + }; + + describe('create', () => { + it('creates an insight with every decimal field coerced', () => { + const insight = StrategicReportInsightDomain.create({ + id: 100, + ...baseProps, + averageScore: '4.5', + averageRating: 90, + averageDelta: -5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(insight.id).toBe(100); + expect(insight.strategicReportId).toBe(1); + expect(insight.insightType).toBe(InsightType.HIGHEST_RATING); + expect(insight.competenceId).toBe(7); + expect(insight.competenceTitle).toBe('Teamwork'); + expect(insight.averageScore).toBeInstanceOf(Decimal); + expect(insight.averageScore!.toString()).toBe('4.5'); + expect(insight.averageRating!.toNumber()).toBe(90); + expect(insight.averageDelta!.toNumber()).toBe(-5); + }); + + it('defaults missing decimal fields to null', () => { + const insight = StrategicReportInsightDomain.create(baseProps); + + expect(insight.averageScore).toBeNull(); + expect(insight.averageRating).toBeNull(); + expect(insight.averageDelta).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/domain/strategic-report.domain.spec.ts b/apps/api/test/unit/contexts/reporting/domain/strategic-report.domain.spec.ts new file mode 100644 index 00000000..e83acae5 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/domain/strategic-report.domain.spec.ts @@ -0,0 +1,106 @@ +import Decimal from 'decimal.js'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; + +describe('StrategicReportDomain', () => { + const baseProps = { + cycleId: 100, + cycleTitle: 'Q1 2025', + rateeCount: 0, + rateeIds: [], + respondentCount: 0, + respondentIds: [], + answerCount: 0, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 0, + competenceIds: [], + questionCount: 0, + questionIds: [], + }; + + describe('create', () => { + it('creates a strategic report with every supplied field', () => { + const report = StrategicReportDomain.create({ + id: 1, + ...baseProps, + rateeCount: 3, + rateeIds: [11, 12, 13], + respondentCount: 5, + respondentIds: [20, 21, 22, 23, 24], + answerCount: 50, + reviewerCount: 2, + reviewerIds: [40, 41], + teamCount: 2, + teamIds: [1, 2], + positionCount: 4, + positionIds: [3, 4, 5, 6], + competenceCount: 7, + competenceIds: [100, 101, 102, 103, 104, 105, 106], + questionCount: 12, + questionIds: [ + 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, + ], + turnoutAvgPctOfRatees: 100, + turnoutAvgPctOfTeams: '90.5', + turnoutAvgPctOfOthers: 88, + competenceGeneralAvgSelf: 4, + competenceGeneralAvgTeam: 3.5, + competenceGeneralAvgOther: 3.9, + competenceGeneralPctSelf: 80, + competenceGeneralPctTeam: 70, + competenceGeneralPctOther: 78, + competenceGeneralDeltaTeam: -10, + competenceGeneralDeltaOther: -2, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + expect(report.id).toBe(1); + expect(report.cycleId).toBe(100); + expect(report.cycleTitle).toBe('Q1 2025'); + expect(report.rateeCount).toBe(3); + expect(report.rateeIds).toEqual([11, 12, 13]); + expect(report.answerCount).toBe(50); + expect(report.turnoutAvgPctOfTeams).toBeInstanceOf(Decimal); + expect(report.turnoutAvgPctOfTeams!.toString()).toBe('90.5'); + expect(report.competenceGeneralDeltaTeam!.toNumber()).toBe(-10); + }); + + it('defaults missing decimal fields to null', () => { + const report = StrategicReportDomain.create(baseProps); + + expect(report.turnoutAvgPctOfRatees).toBeNull(); + expect(report.turnoutAvgPctOfTeams).toBeNull(); + expect(report.competenceGeneralAvgSelf).toBeNull(); + expect(report.competenceGeneralDeltaOther).toBeNull(); + }); + + it('defaults analytics and insights to empty arrays', () => { + const report = StrategicReportDomain.create(baseProps); + + expect(report.analytics).toEqual([]); + expect(report.insights).toEqual([]); + }); + }); + + describe('withAnalytics / withInsights', () => { + it('returns a new instance with the supplied analytics', () => { + const report = StrategicReportDomain.create(baseProps); + const next = report.withAnalytics([{ id: 1 } as any]); + + expect(next).not.toBe(report); + expect(next.analytics).toEqual([{ id: 1 }]); + }); + + it('returns a new instance with the supplied insights', () => { + const report = StrategicReportDomain.create(baseProps); + const next = report.withInsights([{ id: 2 } as any]); + + expect(next).not.toBe(report); + expect(next.insights).toEqual([{ id: 2 }]); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-analytics.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-analytics.mapper.spec.ts new file mode 100644 index 00000000..c911453d --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-analytics.mapper.spec.ts @@ -0,0 +1,100 @@ +import { ReportAnalytics as PrismaReportAnalytics } from '@intra/database'; +import { EntityType } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportAnalyticsMapper } from 'src/contexts/reporting/infrastructure/mappers/report-analytics.mapper'; + +describe('ReportAnalyticsMapper', () => { + const prismaRow = { + id: 1, + reportId: 10, + entityType: 'QUESTION' as PrismaReportAnalytics['entityType'], + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + averageBySelfAssessment: '4.0000', + averageByTeam: '3.5000', + averageByOther: '4.2500', + percentageBySelfAssessment: '80.0000', + percentageByTeam: '70.0000', + percentageByOther: '85.0000', + deltaPercentageByTeam: '-10.0000', + deltaPercentageByOther: '5.0000', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as unknown as PrismaReportAnalytics; + + describe('toDomain', () => { + it('converts a prisma row into a ReportAnalyticsDomain', () => { + const domain = ReportAnalyticsMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(ReportAnalyticsDomain); + expect(domain.id).toBe(1); + expect(domain.reportId).toBe(10); + expect(domain.entityType).toBe(EntityType.QUESTION); + expect(domain.questionId).toBe(100); + expect(domain.averageByTeam!.toNumber()).toBe(3.5); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain object into a Prisma create input with 4dp strings', () => { + const domain = ReportAnalyticsDomain.create({ + reportId: 10, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: 7, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4, + averageByTeam: 3.456789, + averageByOther: 3.9, + percentageBySelfAssessment: 80, + percentageByTeam: 69.5, + percentageByOther: 78, + deltaPercentageByTeam: null, + deltaPercentageByOther: null, + }); + + const prisma = ReportAnalyticsMapper.toPrisma(99, domain); + + expect(prisma.reportId).toBe(99); + expect(prisma.entityType).toBe('COMPETENCE'); + expect(prisma.questionId).toBeUndefined(); + expect(prisma.competenceId).toBe(7); + expect(prisma.averageByTeam).toBe('3.4568'); + expect(prisma.deltaPercentageByTeam).toBeUndefined(); + }); + }); + + describe('entity-type conversions', () => { + it('uppercases the entity type in both directions', () => { + expect( + ReportAnalyticsMapper.toPrismaEntityType(EntityType.QUESTION), + ).toBe('QUESTION'); + expect( + ReportAnalyticsMapper.toDomainEntityType( + 'COMPETENCE' as PrismaReportAnalytics['entityType'], + ), + ).toBe(EntityType.COMPETENCE); + }); + }); + + describe('toDecimalString', () => { + it('returns undefined for null/undefined inputs', () => { + expect(ReportAnalyticsMapper.toDecimalString(null)).toBeUndefined(); + expect( + ReportAnalyticsMapper.toDecimalString(undefined), + ).toBeUndefined(); + }); + + it('rounds to exactly 4 decimal places', () => { + expect(ReportAnalyticsMapper.toDecimalString(3.45678)).toBe( + '3.4568', + ); + expect(ReportAnalyticsMapper.toDecimalString(new Decimal(2))).toBe( + '2.0000', + ); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-comment.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-comment.mapper.spec.ts new file mode 100644 index 00000000..a98767de --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-comment.mapper.spec.ts @@ -0,0 +1,110 @@ +import { ReportComment as PrismaReportComment } from '@intra/database'; +import { CommentSentiment, RespondentCategory } from '@intra/shared-kernel'; +import { ReportCommentDomain } from 'src/contexts/reporting/domain/report-comment.domain'; +import { ReportCommentMapper } from 'src/contexts/reporting/infrastructure/mappers/report-comment.mapper'; + +describe('ReportCommentMapper', () => { + const prismaRow = { + id: 1, + reportId: 10, + questionId: 20, + questionTitle: 'How is teamwork?', + comment: 'Great collaboration', + respondentCategories: [ + 'TEAM', + ] as PrismaReportComment['respondentCategories'], + commentSentiment: 'POSITIVE' as PrismaReportComment['commentSentiment'], + numberOfMentions: 3, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as PrismaReportComment; + + describe('toDomain', () => { + it('converts a prisma row into a ReportCommentDomain', () => { + const domain = ReportCommentMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(ReportCommentDomain); + expect(domain.id).toBe(1); + expect(domain.reportId).toBe(10); + expect(domain.questionId).toBe(20); + expect(domain.comment).toBe('Great collaboration'); + expect(domain.respondentCategories).toEqual([ + RespondentCategory.TEAM, + ]); + expect(domain.commentSentiment).toBe(CommentSentiment.POSITIVE); + }); + + it('falls back commentSentiment to null when prisma returns null', () => { + const domain = ReportCommentMapper.toDomain({ + ...prismaRow, + commentSentiment: null, + } as unknown as PrismaReportComment); + expect(domain.commentSentiment).toBeNull(); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain object into a Prisma create input', () => { + const domain = ReportCommentDomain.create({ + reportId: 10, + questionId: 20, + questionTitle: 'Q?', + comment: 'Nice', + respondentCategories: [RespondentCategory.TEAM], + commentSentiment: CommentSentiment.NEGATIVE, + numberOfMentions: 4, + }); + + const prisma = ReportCommentMapper.toPrisma(domain); + + expect(prisma.reportId).toBe(10); + expect(prisma.questionId).toBe(20); + expect(prisma.questionTitle).toBe('Q?'); + expect(prisma.comment).toBe('Nice'); + expect(prisma.commentSentiment).toBe('NEGATIVE'); + expect(prisma.numberOfMentions).toBe(4); + }); + + it('returns undefined sentiment when domain sentiment is null', () => { + const domain = ReportCommentDomain.create({ + reportId: 10, + questionId: 20, + questionTitle: 'Q?', + comment: 'Nice', + respondentCategories: [RespondentCategory.TEAM], + numberOfMentions: 4, + }); + + const prisma = ReportCommentMapper.toPrisma(domain); + expect(prisma.commentSentiment).toBeUndefined(); + }); + }); + + describe('enum conversions', () => { + it('uppercases respondentCategories in toDomain', () => { + expect( + ReportCommentMapper.toDomainRespondentCategories([ + 'OTHER', + ] as PrismaReportComment['respondentCategories']), + ).toEqual([RespondentCategory.OTHER]); + }); + + it('uppercases sentiment in both directions', () => { + expect( + ReportCommentMapper.toPrismaCommentSentiment( + CommentSentiment.POSITIVE, + ), + ).toBe('POSITIVE'); + expect( + ReportCommentMapper.toDomainCommentSentiment( + 'NEGATIVE' as PrismaReportComment['commentSentiment'], + ), + ).toBe(CommentSentiment.NEGATIVE); + expect( + ReportCommentMapper.toPrismaCommentSentiment(null), + ).toBeUndefined(); + expect( + ReportCommentMapper.toDomainCommentSentiment(null), + ).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-insight.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-insight.mapper.spec.ts new file mode 100644 index 00000000..9c24821a --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-insight.mapper.spec.ts @@ -0,0 +1,104 @@ +import { ReportInsights as PrismaReportInsights } from '@intra/database'; +import { EntityType, InsightType } from '@intra/shared-kernel'; +import { ReportInsightDomain } from 'src/contexts/reporting/domain/report-insight.domain'; +import { ReportInsightMapper } from 'src/contexts/reporting/infrastructure/mappers/report-insight.mapper'; + +describe('ReportInsightMapper', () => { + const prismaRow = { + id: 1, + reportId: 10, + insightType: 'HIGHEST_RATING' as PrismaReportInsights['insightType'], + entityType: 'QUESTION' as PrismaReportInsights['entityType'], + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + averageScore: '4.5000', + averageRating: '90.0000', + averageDelta: '-5.0000', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as unknown as PrismaReportInsights; + + describe('toDomain', () => { + it('converts a prisma row into a ReportInsightDomain', () => { + const domain = ReportInsightMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(ReportInsightDomain); + expect(domain.id).toBe(1); + expect(domain.reportId).toBe(10); + expect(domain.insightType).toBe(InsightType.HIGHEST_RATING); + expect(domain.entityType).toBe(EntityType.QUESTION); + expect(domain.questionId).toBe(100); + expect(domain.averageScore!.toNumber()).toBe(4.5); + expect(domain.averageRating!.toNumber()).toBe(90); + expect(domain.averageDelta!.toNumber()).toBe(-5); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain object into a Prisma create input', () => { + const domain = ReportInsightDomain.create({ + reportId: 10, + insightType: InsightType.LOWEST_DELTA, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: 7, + competenceTitle: 'Teamwork', + averageScore: 4, + averageRating: 80, + averageDelta: -10, + }); + + const prisma = ReportInsightMapper.toPrisma(99, domain); + + expect(prisma.reportId).toBe(99); + expect(prisma.insightType).toBe('LOWEST_DELTA'); + expect(prisma.entityType).toBe('COMPETENCE'); + expect(prisma.questionId).toBeUndefined(); + expect(prisma.competenceId).toBe(7); + expect(prisma.averageScore).toBe('4.0000'); + expect(prisma.averageRating).toBe('80.0000'); + expect(prisma.averageDelta).toBe('-10.0000'); + }); + }); + + describe('enum conversions', () => { + it('uppercases the entity type in both directions', () => { + expect( + ReportInsightMapper.toPrismaEntityType(EntityType.QUESTION), + ).toBe('QUESTION'); + expect( + ReportInsightMapper.toDomainEntityType( + 'COMPETENCE' as PrismaReportInsights['entityType'], + ), + ).toBe(EntityType.COMPETENCE); + }); + + it('uppercases the insight type in both directions', () => { + expect( + ReportInsightMapper.toPrismaInsightType( + InsightType.HIGHEST_DELTA, + ), + ).toBe('HIGHEST_DELTA'); + expect( + ReportInsightMapper.toDomainInsightType( + 'LOWEST_RATING' as PrismaReportInsights['insightType'], + ), + ).toBe(InsightType.LOWEST_RATING); + }); + }); + + describe('toDecimalString', () => { + it('returns undefined for null/undefined inputs', () => { + expect(ReportInsightMapper.toDecimalString(null)).toBeUndefined(); + expect( + ReportInsightMapper.toDecimalString(undefined), + ).toBeUndefined(); + }); + + it('rounds to 4 decimal places', () => { + expect(ReportInsightMapper.toDecimalString(3.45678)).toBe('3.4568'); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report.mapper.spec.ts new file mode 100644 index 00000000..16a5b400 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report.mapper.spec.ts @@ -0,0 +1,172 @@ +import { + Report as PrismaReport, + ReportAnalytics as PrismaReportAnalytics, + ReportComment as PrismaReportComment, + ReportInsights as PrismaReportInsights, +} from '@intra/database'; +import { RespondentCategory } from '@intra/shared-kernel'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportMapper } from 'src/contexts/reporting/infrastructure/mappers/report.mapper'; + +function buildPrismaReport( + overrides: Partial = {}, +): PrismaReport { + return { + id: 1, + reviewId: 10, + cycleId: 100, + respondentCount: 5, + respondentCategories: [ + 'TEAM', + 'OTHER', + ] as PrismaReport['respondentCategories'], + answerCount: 20, + turnoutPctOfTeam: '75.5000', + turnoutPctOfOther: '80.0000', + questionTotAvgBySelf: '4.0000', + questionTotAvgByTeam: '3.5000', + questionTotAvgByOthers: '4.2000', + questionTotPctBySelf: '80.0000', + questionTotPctByTeam: '70.0000', + questionTotPctByOthers: '84.0000', + questionTotDeltaPctByTeam: '-10.0000', + questionTotDeltaPctByOthers: '4.0000', + competenceTotAvgBySelf: '4.5000', + competenceTotAvgByTeam: '3.8000', + competenceTotAvgByOthers: '4.1000', + competenceTotPctBySelf: '90.0000', + competenceTotPctByTeam: '76.0000', + competenceTotPctByOthers: '82.0000', + competenceTotDeltaPctByTeam: '-14.0000', + competenceTotDeltaPctByOthers: '-8.0000', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + ...overrides, + } as unknown as PrismaReport; +} + +describe('ReportMapper', () => { + describe('toDomain', () => { + it('converts a prisma row into a ReportDomain with empty relations', () => { + const domain = ReportMapper.toDomain(buildPrismaReport()); + + expect(domain).toBeInstanceOf(ReportDomain); + expect(domain.id).toBe(1); + expect(domain.reviewId).toBe(10); + expect(domain.cycleId).toBe(100); + expect(domain.respondentCount).toBe(5); + expect(domain.respondentCategories).toEqual([ + RespondentCategory.TEAM, + RespondentCategory.OTHER, + ]); + expect(domain.turnoutPctOfTeam!.toNumber()).toBe(75.5); + expect(domain.competenceTotDeltaPctByOthers!.toNumber()).toBe(-8); + expect(domain.analytics).toEqual([]); + expect(domain.comments).toEqual([]); + expect(domain.insights).toEqual([]); + }); + }); + + describe('toDomainWithRelations', () => { + it('includes analytics, comments, and insights in the aggregate', () => { + const prismaAnalytic = { + id: 1, + reportId: 1, + entityType: 'QUESTION', + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + averageBySelfAssessment: '4.0000', + averageByTeam: '3.5000', + averageByOther: '4.2500', + percentageBySelfAssessment: '80.0000', + percentageByTeam: '70.0000', + percentageByOther: '85.0000', + deltaPercentageByTeam: '-10.0000', + deltaPercentageByOther: '5.0000', + createdAt: new Date(), + } as unknown as PrismaReportAnalytics; + + const prismaComment = { + id: 1, + reportId: 1, + questionId: 100, + questionTitle: 'Q?', + comment: 'Nice', + respondentCategories: ['TEAM'], + commentSentiment: 'POSITIVE', + numberOfMentions: 1, + createdAt: new Date(), + } as unknown as PrismaReportComment; + + const prismaInsight = { + id: 1, + reportId: 1, + insightType: 'HIGHEST_RATING', + entityType: 'QUESTION', + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + averageScore: '4.5000', + averageRating: '90.0000', + averageDelta: '-5.0000', + createdAt: new Date(), + } as unknown as PrismaReportInsights; + + const domain = ReportMapper.toDomainWithRelations({ + ...buildPrismaReport(), + analytics: [prismaAnalytic], + comments: [prismaComment], + insights: [prismaInsight], + }); + + expect(domain.analytics).toHaveLength(1); + expect(domain.analytics[0].questionId).toBe(100); + expect(domain.comments).toHaveLength(1); + expect(domain.comments![0].comment).toBe('Nice'); + expect(domain.insights).toHaveLength(1); + expect(domain.insights![0].averageRating!.toNumber()).toBe(90); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain object into a Prisma create input', () => { + const domain = ReportDomain.create({ + reviewId: 10, + cycleId: 100, + respondentCount: 5, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 20, + turnoutPctOfTeam: 75.5, + turnoutPctOfOther: 80, + analytics: [], + }); + + const prisma = ReportMapper.toPrisma(domain); + + expect(prisma.reviewId).toBe(10); + expect(prisma.cycleId).toBe(100); + expect(prisma.respondentCount).toBe(5); + expect(prisma.respondentCategories).toEqual(['TEAM']); + expect(prisma.answerCount).toBe(20); + expect(prisma.turnoutPctOfTeam).toBe('75.5000'); + expect(prisma.turnoutPctOfOther).toBe('80.0000'); + // Missing decimal field → undefined + expect(prisma.questionTotAvgBySelf).toBeUndefined(); + }); + }); + + describe('respondent category conversions', () => { + it('uppercases in both directions', () => { + expect( + ReportMapper.toPrismaRespondentCategory( + RespondentCategory.TEAM, + ), + ).toBe('TEAM'); + expect( + ReportMapper.toDomainRespondentCategory('OTHER' as any), + ).toBe(RespondentCategory.OTHER); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report-analytics.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report-analytics.mapper.spec.ts new file mode 100644 index 00000000..39fc440b --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report-analytics.mapper.spec.ts @@ -0,0 +1,72 @@ +import { StrategicReportAnalytics as PrismaStrategicReportAnalytics } from '@intra/database'; +import { StrategicReportAnalyticsDomain } from 'src/contexts/reporting/domain/strategic-report-analytics.domain'; +import { StrategicReportAnalyticsMapper } from 'src/contexts/reporting/infrastructure/mappers/strategic-report-analytics.mapper'; + +describe('StrategicReportAnalyticsMapper', () => { + const prismaRow = { + id: 1, + strategicReportId: 10, + competenceId: 7, + competenceTitle: 'Teamwork', + averageBySelfAssessment: '4.0000', + averageByTeam: '3.5000', + averageByOther: '3.9000', + percentageBySelfAssessment: '80.0000', + percentageByTeam: '70.0000', + percentageByOther: '78.0000', + deltaPercentageByTeam: '-10.0000', + deltaPercentageByOther: '-2.0000', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as unknown as PrismaStrategicReportAnalytics; + + describe('toDomain', () => { + it('converts a prisma row into a StrategicReportAnalyticsDomain', () => { + const domain = StrategicReportAnalyticsMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(StrategicReportAnalyticsDomain); + expect(domain.id).toBe(1); + expect(domain.strategicReportId).toBe(10); + expect(domain.competenceId).toBe(7); + expect(domain.competenceTitle).toBe('Teamwork'); + expect(domain.averageByTeam!.toNumber()).toBe(3.5); + }); + }); + + describe('toPrisma', () => { + it('serialises with 4dp strings and undefined for null fields', () => { + const domain = StrategicReportAnalyticsDomain.create({ + strategicReportId: 0, + competenceId: 7, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4, + averageByTeam: 3.45678, + }); + + const prisma = StrategicReportAnalyticsMapper.toPrisma(99, domain); + + expect(prisma.strategicReportId).toBe(99); + expect(prisma.competenceId).toBe(7); + expect(prisma.competenceTitle).toBe('Teamwork'); + expect(prisma.averageBySelfAssessment).toBe('4.0000'); + expect(prisma.averageByTeam).toBe('3.4568'); + expect(prisma.averageByOther).toBeUndefined(); + }); + }); + + describe('toDecimalString', () => { + it('returns undefined for null/undefined', () => { + expect( + StrategicReportAnalyticsMapper.toDecimalString(null), + ).toBeUndefined(); + expect( + StrategicReportAnalyticsMapper.toDecimalString(undefined), + ).toBeUndefined(); + }); + + it('rounds to 4 decimal places', () => { + expect( + StrategicReportAnalyticsMapper.toDecimalString(3.45678), + ).toBe('3.4568'); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report-insight.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report-insight.mapper.spec.ts new file mode 100644 index 00000000..b9b092d5 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report-insight.mapper.spec.ts @@ -0,0 +1,82 @@ +import { StrategicReportInsights as PrismaStrategicReportInsights } from '@intra/database'; +import { EntityType, InsightType } from '@intra/shared-kernel'; +import { StrategicReportInsightDomain } from 'src/contexts/reporting/domain/startegic-report-insight.domain'; +import { StrategicReportInsightMapper } from 'src/contexts/reporting/infrastructure/mappers/strategic-report-insight.mapper'; + +describe('StrategicReportInsightMapper', () => { + const prismaRow = { + id: 1, + strategicReportId: 10, + insightType: + 'HIGHEST_RATING' as PrismaStrategicReportInsights['insightType'], + competenceId: 7, + competenceTitle: 'Teamwork', + averageScore: '4.5000', + averageRating: '90.0000', + averageDelta: '-5.0000', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + } as unknown as PrismaStrategicReportInsights; + + describe('toDomain', () => { + it('converts a prisma row into a StrategicReportInsightDomain', () => { + const domain = StrategicReportInsightMapper.toDomain(prismaRow); + + expect(domain).toBeInstanceOf(StrategicReportInsightDomain); + expect(domain.id).toBe(1); + expect(domain.strategicReportId).toBe(10); + expect(domain.insightType).toBe(InsightType.HIGHEST_RATING); + expect(domain.competenceId).toBe(7); + expect(domain.averageScore!.toNumber()).toBe(4.5); + expect(domain.averageRating!.toNumber()).toBe(90); + expect(domain.averageDelta!.toNumber()).toBe(-5); + }); + }); + + describe('toPrisma', () => { + it('serialises with 4dp strings and undefined for null fields', () => { + const domain = StrategicReportInsightDomain.create({ + strategicReportId: 0, + insightType: InsightType.LOWEST_DELTA, + competenceId: 7, + competenceTitle: 'Teamwork', + averageScore: 4.5, + }); + + const prisma = StrategicReportInsightMapper.toPrisma(99, domain); + + expect(prisma.strategicReportId).toBe(99); + expect(prisma.insightType).toBe('LOWEST_DELTA'); + expect(prisma.competenceId).toBe(7); + expect(prisma.averageScore).toBe('4.5000'); + expect(prisma.averageRating).toBeUndefined(); + }); + }); + + describe('enum conversions', () => { + it('uppercases entity type in both directions', () => { + expect( + StrategicReportInsightMapper.toPrismaEntityType( + EntityType.COMPETENCE, + ), + ).toBe('COMPETENCE'); + expect( + StrategicReportInsightMapper.toDomainEntityType( + 'QUESTION' as any, + ), + ).toBe(EntityType.QUESTION); + }); + + it('uppercases insight type in both directions', () => { + expect( + StrategicReportInsightMapper.toPrismaInsightType( + InsightType.LOWEST_RATING, + ), + ).toBe('LOWEST_RATING'); + expect( + StrategicReportInsightMapper.toDomainInsightType( + 'HIGHEST_DELTA' as PrismaStrategicReportInsights['insightType'], + ), + ).toBe(InsightType.HIGHEST_DELTA); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report.mapper.spec.ts new file mode 100644 index 00000000..2ecb45c0 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report.mapper.spec.ts @@ -0,0 +1,140 @@ +import { + StrategicReport as PrismaStrategicReport, + StrategicReportAnalytics as PrismaStrategicReportAnalytics, + StrategicReportInsights as PrismaStrategicReportInsights, +} from '@intra/database'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { StrategicReportMapper } from 'src/contexts/reporting/infrastructure/mappers/strategic-report.mapper'; + +function buildPrismaReport( + overrides: Partial = {}, +): PrismaStrategicReport { + return { + id: 1, + cycleId: 100, + cycleTitle: 'Q1 2025', + rateeCount: 3, + rateeIds: [11, 12, 13], + respondentCount: 5, + respondentIds: [20, 21, 22, 23, 24], + answerCount: 50, + reviewerCount: 2, + reviewerIds: [40, 41], + teamCount: 2, + teamIds: [1, 2], + positionCount: 4, + positionIds: [3, 4, 5, 6], + competenceCount: 1, + competenceIds: [100], + questionCount: 2, + questionIds: [200, 201], + turnoutAvgPctOfRatees: '100.0000', + turnoutAvgPctOfTeams: '90.5000', + turnoutAvgPctOfOthers: '88.0000', + competenceGeneralAvgSelf: '4.0000', + competenceGeneralAvgTeam: '3.5000', + competenceGeneralAvgOther: '3.9000', + competenceGeneralPctSelf: '80.0000', + competenceGeneralPctTeam: '70.0000', + competenceGeneralPctOther: '78.0000', + competenceGeneralDeltaTeam: '-10.0000', + competenceGeneralDeltaOther: '-2.0000', + createdAt: new Date('2025-01-01T00:00:00.000Z'), + ...overrides, + } as unknown as PrismaStrategicReport; +} + +describe('StrategicReportMapper', () => { + describe('toDomain', () => { + it('converts a prisma row into a StrategicReportDomain with empty relations', () => { + const domain = StrategicReportMapper.toDomain(buildPrismaReport()); + + expect(domain).toBeInstanceOf(StrategicReportDomain); + expect(domain.id).toBe(1); + expect(domain.cycleId).toBe(100); + expect(domain.cycleTitle).toBe('Q1 2025'); + expect(domain.rateeIds).toEqual([11, 12, 13]); + expect(domain.competenceGeneralAvgSelf!.toNumber()).toBe(4); + expect(domain.analytics).toEqual([]); + expect(domain.insights).toEqual([]); + }); + }); + + describe('toDomainWithRelations', () => { + it('includes analytics and insights in the aggregate', () => { + const prismaAnalytic = { + id: 1, + strategicReportId: 1, + competenceId: 7, + competenceTitle: 'Teamwork', + averageBySelfAssessment: '4.0000', + averageByTeam: '3.5000', + averageByOther: '3.9000', + percentageBySelfAssessment: '80.0000', + percentageByTeam: '70.0000', + percentageByOther: '78.0000', + deltaPercentageByTeam: '-10.0000', + deltaPercentageByOther: '-2.0000', + createdAt: new Date(), + } as unknown as PrismaStrategicReportAnalytics; + + const prismaInsight = { + id: 1, + strategicReportId: 1, + insightType: 'HIGHEST_RATING', + competenceId: 7, + competenceTitle: 'Teamwork', + averageScore: '4.5000', + averageRating: '90.0000', + averageDelta: '-5.0000', + createdAt: new Date(), + } as unknown as PrismaStrategicReportInsights; + + const domain = StrategicReportMapper.toDomainWithRelations({ + ...buildPrismaReport(), + analytics: [prismaAnalytic], + insights: [prismaInsight], + }); + + expect(domain.analytics).toHaveLength(1); + expect(domain.analytics![0].competenceTitle).toBe('Teamwork'); + expect(domain.insights).toHaveLength(1); + expect(domain.insights![0].averageRating!.toNumber()).toBe(90); + }); + }); + + describe('toPrisma', () => { + it('serialises a domain object into a Prisma create input', () => { + const domain = StrategicReportDomain.create({ + cycleId: 100, + cycleTitle: 'Q1', + rateeCount: 1, + rateeIds: [1], + respondentCount: 0, + respondentIds: [], + answerCount: 0, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 0, + competenceIds: [], + questionCount: 0, + questionIds: [], + turnoutAvgPctOfRatees: 100, + competenceGeneralAvgSelf: 4, + }); + + const prisma = StrategicReportMapper.toPrisma(domain); + + expect(prisma.cycleId).toBe(100); + expect(prisma.cycleTitle).toBe('Q1'); + expect(prisma.rateeIds).toEqual([1]); + expect(prisma.turnoutAvgPctOfRatees).toBe('100.0000'); + expect(prisma.competenceGeneralAvgSelf).toBe('4.0000'); + expect(prisma.turnoutAvgPctOfTeams).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/controllers/reporting.controller.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/controllers/reporting.controller.spec.ts new file mode 100644 index 00000000..c1d8f52a --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/controllers/reporting.controller.spec.ts @@ -0,0 +1,278 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { + CommentSentiment, + EntityType, + IdentityRole, + InsightType, + RespondentCategory, +} from '@intra/shared-kernel'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { ReportAnalyticsService } from 'src/contexts/reporting/application/services/report-analytics.service'; +import { ReportCommentService } from 'src/contexts/reporting/application/services/report-comment.service'; +import { ReportInsightService } from 'src/contexts/reporting/application/services/report-insight.service'; +import { ReportingService } from 'src/contexts/reporting/application/services/reports.service'; +import { TextAnswerService } from 'src/contexts/reporting/application/services/text-answer.service'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportCommentDomain } from 'src/contexts/reporting/domain/report-comment.domain'; +import { ReportInsightDomain } from 'src/contexts/reporting/domain/report-insight.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportingController } from 'src/contexts/reporting/presentation/http/controllers/reporting.controller'; + +function buildReport(): ReportDomain { + return ReportDomain.create({ + id: 1, + reviewId: 10, + cycleId: 100, + respondentCount: 5, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 20, + analytics: [], + comments: [], + insights: [], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); +} + +function buildAnalytics(): ReportAnalyticsDomain { + return ReportAnalyticsDomain.create({ + id: 1, + reportId: 10, + entityType: EntityType.QUESTION, + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + }); +} + +function buildInsight(): ReportInsightDomain { + return ReportInsightDomain.create({ + id: 1, + reportId: 10, + insightType: InsightType.HIGHEST_RATING, + entityType: EntityType.QUESTION, + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + }); +} + +function buildComment(): ReportCommentDomain { + return ReportCommentDomain.create({ + id: 1, + reportId: 10, + questionId: 100, + questionTitle: 'Q?', + comment: 'Nice', + respondentCategories: [RespondentCategory.TEAM], + commentSentiment: CommentSentiment.POSITIVE, + numberOfMentions: 1, + }); +} + +describe('ReportingController', () => { + let controller: ReportingController; + let reporting: any; + let textAnswers: any; + let analytics: any; + let insights: any; + let comments: any; + const actor = UserDomain.create({ + id: 7, + firstName: 'HR', + lastName: 'Admin', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + + beforeEach(() => { + reporting = { + search: jest.fn(), + getById: jest.fn(), + getByReviewId: jest.fn(), + }; + textAnswers = { listByReview: jest.fn() }; + analytics = { getByReportId: jest.fn(), getById: jest.fn() }; + insights = { getByReportId: jest.fn(), getById: jest.fn() }; + comments = { + getByReportId: jest.fn(), + getById: jest.fn(), + create: jest.fn(), + }; + + controller = new ReportingController( + reporting as unknown as ReportingService, + textAnswers as unknown as TextAnswerService, + analytics as unknown as ReportAnalyticsService, + insights as unknown as ReportInsightService, + comments as unknown as ReportCommentService, + ); + }); + + describe('search', () => { + it('forwards query/actor and maps results', async () => { + reporting.search.mockResolvedValue([buildReport()]); + + const result = await controller.search({} as any, actor); + + expect(reporting.search).toHaveBeenCalledWith({}, actor); + expect(result).toHaveLength(1); + expect(result[0].id).toBe(1); + }); + }); + + describe('getById', () => { + it('forwards id/actor to the service', async () => { + reporting.getById.mockResolvedValue(buildReport()); + + const result = await controller.getById(1, actor); + + expect(reporting.getById).toHaveBeenCalledWith(1, actor); + expect(result.id).toBe(1); + }); + }); + + describe('getByReviewId', () => { + it('forwards review id/actor to the service', async () => { + reporting.getByReviewId.mockResolvedValue(buildReport()); + + const result = await controller.getByReviewId(10, actor); + + expect(reporting.getByReviewId).toHaveBeenCalledWith(10, actor); + expect(result.reviewId).toBe(10); + }); + }); + + describe('getTextAnswers', () => { + it('maps the text answers from the service', async () => { + textAnswers.listByReview.mockResolvedValue([ + { + questionId: 1, + questionTitle: 'Q?', + respondentCategory: RespondentCategory.TEAM, + textValue: 'Great', + }, + ]); + + const result = await controller.getTextAnswers(10, actor); + + expect(textAnswers.listByReview).toHaveBeenCalledWith(10, actor); + expect(result).toHaveLength(1); + expect(result[0].textValue).toBe('Great'); + }); + }); + + describe('listReportAnalytics', () => { + it('maps every analytics row to a response', async () => { + analytics.getByReportId.mockResolvedValue([buildAnalytics()]); + + const result = await controller.listReportAnalytics(1, actor); + + expect(analytics.getByReportId).toHaveBeenCalledWith(1, actor); + expect(result).toHaveLength(1); + }); + }); + + describe('getReportAnalyticsById', () => { + it('forwards id and maps the response', async () => { + analytics.getById.mockResolvedValue(buildAnalytics()); + + const result = await controller.getReportAnalyticsById(1, actor); + + expect(analytics.getById).toHaveBeenCalledWith(1, actor); + expect(result.id).toBe(1); + }); + }); + + describe('listReportInsights', () => { + it('maps every insight to a response', async () => { + insights.getByReportId.mockResolvedValue([buildInsight()]); + + const result = await controller.listReportInsights(1, actor); + + expect(insights.getByReportId).toHaveBeenCalledWith(1, actor); + expect(result).toHaveLength(1); + }); + }); + + describe('getReportInsightById', () => { + it('forwards id and maps the response', async () => { + insights.getById.mockResolvedValue(buildInsight()); + + const result = await controller.getReportInsightById(1, actor); + + expect(insights.getById).toHaveBeenCalledWith(1, actor); + expect(result.id).toBe(1); + }); + }); + + describe('listReportComments', () => { + it('maps every comment to a response', async () => { + comments.getByReportId.mockResolvedValue([buildComment()]); + + const result = await controller.listReportComments(1, actor); + + expect(comments.getByReportId).toHaveBeenCalledWith(1, actor); + expect(result).toHaveLength(1); + }); + }); + + describe('getReportCommentById', () => { + it('forwards id and maps the response', async () => { + comments.getById.mockResolvedValue(buildComment()); + + const result = await controller.getReportCommentById(1); + + expect(comments.getById).toHaveBeenCalledWith(1); + expect(result.id).toBe(1); + }); + }); + + describe('create', () => { + it('builds a domain comment from the dto and forwards to the service', async () => { + comments.create.mockImplementation( + async (c: ReportCommentDomain) => c, + ); + + const result = await controller.create(10, { + reportId: 10, + questionId: 100, + questionTitle: 'Q?', + comment: 'Nice', + respondentCategories: [RespondentCategory.TEAM], + commentSentiment: CommentSentiment.POSITIVE, + numberOfMentions: 2, + } as any); + + expect(comments.create).toHaveBeenCalledTimes(1); + const created = comments.create.mock.calls[0][0]; + expect(created).toBeInstanceOf(ReportCommentDomain); + expect(created.reportId).toBe(10); + expect(created.commentSentiment).toBe(CommentSentiment.POSITIVE); + expect(result.comment).toBe('Nice'); + }); + + it('defaults sentiment to null when omitted from the dto', async () => { + comments.create.mockImplementation( + async (c: ReportCommentDomain) => c, + ); + + await controller.create(10, { + reportId: 10, + questionId: 100, + questionTitle: 'Q?', + comment: 'X', + respondentCategories: [RespondentCategory.TEAM], + numberOfMentions: 1, + } as any); + + const created = comments.create.mock.calls[0][0]; + expect(created.commentSentiment).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/controllers/strategic-reporting.controller.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/controllers/strategic-reporting.controller.spec.ts new file mode 100644 index 00000000..3e70c411 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/controllers/strategic-reporting.controller.spec.ts @@ -0,0 +1,194 @@ +jest.mock('better-auth', () => ({ betterAuth: jest.fn(() => ({})) })); +jest.mock('better-auth/adapters/prisma', () => ({ + prismaAdapter: jest.fn(() => ({})), +})); + +import { IdentityRole, InsightType } from '@intra/shared-kernel'; +import { UserDomain } from 'src/contexts/identity/domain/user.domain'; +import { StartegicReportInsightService } from 'src/contexts/reporting/application/services/startegic-report-insight.service'; +import { StrategicReportAnalyticsService } from 'src/contexts/reporting/application/services/strategic-report-analytics.service'; +import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service'; +import { StrategicReportInsightDomain } from 'src/contexts/reporting/domain/startegic-report-insight.domain'; +import { StrategicReportAnalyticsDomain } from 'src/contexts/reporting/domain/strategic-report-analytics.domain'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { StrategicReportingController } from 'src/contexts/reporting/presentation/http/controllers/strategic-reporting.controller'; + +function buildReport(): StrategicReportDomain { + return StrategicReportDomain.create({ + id: 1, + cycleId: 100, + cycleTitle: 'Q1', + rateeCount: 1, + rateeIds: [11], + respondentCount: 2, + respondentIds: [20, 21], + answerCount: 50, + reviewerCount: 1, + reviewerIds: [40], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 0, + competenceIds: [], + questionCount: 0, + questionIds: [], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); +} + +function buildAnalytics(): StrategicReportAnalyticsDomain { + return StrategicReportAnalyticsDomain.create({ + id: 1, + strategicReportId: 10, + competenceId: 7, + competenceTitle: 'Teamwork', + }); +} + +function buildInsight(): StrategicReportInsightDomain { + return StrategicReportInsightDomain.create({ + id: 1, + strategicReportId: 10, + insightType: InsightType.HIGHEST_RATING, + competenceId: 7, + competenceTitle: 'Teamwork', + }); +} + +describe('StrategicReportingController', () => { + let controller: StrategicReportingController; + let strategicReporting: any; + let strategicAnalytics: any; + let strategicInsights: any; + const actor = UserDomain.create({ + id: 7, + firstName: 'HR', + lastName: 'Admin', + email: 'hr@example.com', + roles: [IdentityRole.HR], + }); + + beforeEach(() => { + strategicReporting = { + search: jest.fn(), + getById: jest.fn(), + getByCycleId: jest.fn(), + }; + strategicAnalytics = { + getByStrategicReportId: jest.fn(), + getById: jest.fn(), + }; + strategicInsights = { + getByStrategicReportId: jest.fn(), + getById: jest.fn(), + }; + + controller = new StrategicReportingController( + strategicReporting as unknown as StrategicReportingService, + strategicAnalytics as unknown as StrategicReportAnalyticsService, + strategicInsights as unknown as StartegicReportInsightService, + ); + }); + + describe('search', () => { + it('forwards query/actor and maps results', async () => { + strategicReporting.search.mockResolvedValue([buildReport()]); + + const result = await controller.search({} as any, actor); + + expect(strategicReporting.search).toHaveBeenCalledWith({}, actor); + expect(result).toHaveLength(1); + expect(result[0].id).toBe(1); + }); + }); + + describe('getById', () => { + it('forwards id/actor to the service', async () => { + strategicReporting.getById.mockResolvedValue(buildReport()); + + const result = await controller.getById(1, actor); + + expect(strategicReporting.getById).toHaveBeenCalledWith(1, actor); + expect(result.id).toBe(1); + }); + }); + + describe('getByCycleId', () => { + it('forwards cycle id/actor to the service', async () => { + strategicReporting.getByCycleId.mockResolvedValue(buildReport()); + + const result = await controller.getByCycleId(100, actor); + + expect(strategicReporting.getByCycleId).toHaveBeenCalledWith( + 100, + actor, + ); + expect(result.cycleId).toBe(100); + }); + }); + + describe('listStrategicReportAnalytics', () => { + it('maps every analytics row to a response', async () => { + strategicAnalytics.getByStrategicReportId.mockResolvedValue([ + buildAnalytics(), + ]); + + const result = await controller.listStrategicReportAnalytics( + 1, + actor, + ); + + expect( + strategicAnalytics.getByStrategicReportId, + ).toHaveBeenCalledWith(1, actor); + expect(result).toHaveLength(1); + }); + }); + + describe('getStrategicReportAnalyticsById', () => { + it('forwards id and maps the response', async () => { + strategicAnalytics.getById.mockResolvedValue(buildAnalytics()); + + const result = await controller.getStrategicReportAnalyticsById( + 1, + actor, + ); + + expect(strategicAnalytics.getById).toHaveBeenCalledWith(1, actor); + expect(result.id).toBe(1); + }); + }); + + describe('listStrategicReportInsights', () => { + it('maps every insight to a response', async () => { + strategicInsights.getByStrategicReportId.mockResolvedValue([ + buildInsight(), + ]); + + const result = await controller.listStrategicReportInsights( + 1, + actor, + ); + + expect( + strategicInsights.getByStrategicReportId, + ).toHaveBeenCalledWith(1, actor); + expect(result).toHaveLength(1); + }); + }); + + describe('getStrategicReportInsightById', () => { + it('forwards id and maps the response', async () => { + strategicInsights.getById.mockResolvedValue(buildInsight()); + + const result = await controller.getStrategicReportInsightById( + 1, + actor, + ); + + expect(strategicInsights.getById).toHaveBeenCalledWith(1, actor); + expect(result.id).toBe(1); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/mappers/report-analytics.http.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/mappers/report-analytics.http.mapper.spec.ts new file mode 100644 index 00000000..dc6d4220 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/mappers/report-analytics.http.mapper.spec.ts @@ -0,0 +1,77 @@ +import { EntityType } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportAnalyticsHttpMapper } from 'src/contexts/reporting/presentation/http/mappers/report-analytics.http.mapper'; + +describe('ReportAnalyticsHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field, rounding decimals to 4dp', () => { + const domain = ReportAnalyticsDomain.create({ + id: 1, + reportId: 10, + entityType: EntityType.QUESTION, + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + averageBySelfAssessment: 4, + averageByTeam: 3.456789, + averageByOther: 3.9, + percentageBySelfAssessment: 80, + percentageByTeam: 69.5, + percentageByOther: 78, + deltaPercentageByTeam: -10, + deltaPercentageByOther: -2, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = ReportAnalyticsHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.reportId).toBe(10); + expect(response.entityType).toBe(EntityType.QUESTION); + expect(response.questionId).toBe(100); + expect(response.competenceId).toBeNull(); + expect(response.averageByTeam).toBeCloseTo(3.4568, 4); + expect(response.percentageBySelfAssessment).toBe(80); + expect(response.deltaPercentageByTeam).toBe(-10); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('returns null for missing decimal fields', () => { + const domain = ReportAnalyticsDomain.create({ + reportId: 10, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: 7, + competenceTitle: 'Teamwork', + }); + + const response = ReportAnalyticsHttpMapper.toResponse(domain); + + expect(response.averageBySelfAssessment).toBeNull(); + expect(response.averageByTeam).toBeNull(); + expect(response.deltaPercentageByOther).toBeNull(); + }); + }); + + describe('roundScore', () => { + it('rounds a Decimal value to 4 decimal places', () => { + expect( + ReportAnalyticsHttpMapper.roundScore(new Decimal('3.456789')), + ).toBeCloseTo(3.4568, 4); + }); + + it('accepts a plain number', () => { + expect(ReportAnalyticsHttpMapper.roundScore(2.5)).toBe(2.5); + }); + + it('returns null for null or undefined', () => { + expect(ReportAnalyticsHttpMapper.roundScore(null)).toBeNull(); + expect(ReportAnalyticsHttpMapper.roundScore(undefined)).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/mappers/report-comment.http.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/mappers/report-comment.http.mapper.spec.ts new file mode 100644 index 00000000..f03078a4 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/mappers/report-comment.http.mapper.spec.ts @@ -0,0 +1,51 @@ +import { CommentSentiment, RespondentCategory } from '@intra/shared-kernel'; +import { ReportCommentDomain } from 'src/contexts/reporting/domain/report-comment.domain'; +import { ReportCommentHttpMapper } from 'src/contexts/reporting/presentation/http/mappers/report-comment.http.mapper'; + +describe('ReportCommentHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response', () => { + const domain = ReportCommentDomain.create({ + id: 1, + reportId: 10, + questionId: 20, + questionTitle: 'How is teamwork?', + comment: 'Great collaboration', + respondentCategories: [RespondentCategory.TEAM], + commentSentiment: CommentSentiment.POSITIVE, + numberOfMentions: 3, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = ReportCommentHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.reportId).toBe(10); + expect(response.questionId).toBe(20); + expect(response.questionTitle).toBe('How is teamwork?'); + expect(response.comment).toBe('Great collaboration'); + expect(response.respondentCategories).toEqual([ + RespondentCategory.TEAM, + ]); + expect(response.commentSentiment).toBe(CommentSentiment.POSITIVE); + expect(response.numberOfMentions).toBe(3); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('returns null sentiment when none is set on the domain', () => { + const domain = ReportCommentDomain.create({ + reportId: 10, + questionId: 20, + questionTitle: 'Q?', + comment: 'X', + respondentCategories: [RespondentCategory.TEAM], + numberOfMentions: 1, + }); + + const response = ReportCommentHttpMapper.toResponse(domain); + expect(response.commentSentiment).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/mappers/report-insight.http.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/mappers/report-insight.http.mapper.spec.ts new file mode 100644 index 00000000..67f2dd7a --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/mappers/report-insight.http.mapper.spec.ts @@ -0,0 +1,68 @@ +import { EntityType, InsightType } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { ReportInsightDomain } from 'src/contexts/reporting/domain/report-insight.domain'; +import { ReportInsightHttpMapper } from 'src/contexts/reporting/presentation/http/mappers/report-insight.http.mapper'; + +describe('ReportInsightHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field, rounding decimals to 4dp', () => { + const domain = ReportInsightDomain.create({ + id: 1, + reportId: 10, + insightType: InsightType.HIGHEST_RATING, + entityType: EntityType.QUESTION, + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + averageScore: 4.5, + averageRating: 90, + averageDelta: -5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = ReportInsightHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.reportId).toBe(10); + expect(response.insightType).toBe(InsightType.HIGHEST_RATING); + expect(response.entityType).toBe(EntityType.QUESTION); + expect(response.questionId).toBe(100); + expect(response.competenceId).toBeNull(); + expect(response.averageScore).toBe(4.5); + expect(response.averageRating).toBe(90); + expect(response.averageDelta).toBe(-5); + }); + + it('returns null for missing decimal fields', () => { + const domain = ReportInsightDomain.create({ + reportId: 10, + insightType: InsightType.LOWEST_DELTA, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: 7, + competenceTitle: 'Teamwork', + }); + + const response = ReportInsightHttpMapper.toResponse(domain); + + expect(response.averageScore).toBeNull(); + expect(response.averageRating).toBeNull(); + expect(response.averageDelta).toBeNull(); + }); + }); + + describe('roundScore', () => { + it('rounds a Decimal value to 4 decimal places', () => { + expect( + ReportInsightHttpMapper.roundScore(new Decimal('3.456789')), + ).toBeCloseTo(3.4568, 4); + }); + + it('returns null for null and undefined', () => { + expect(ReportInsightHttpMapper.roundScore(null)).toBeNull(); + expect(ReportInsightHttpMapper.roundScore(undefined)).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/mappers/report.http.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/mappers/report.http.mapper.spec.ts new file mode 100644 index 00000000..ed6f128a --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/mappers/report.http.mapper.spec.ts @@ -0,0 +1,172 @@ +import { + EntityType, + InsightType, + RespondentCategory, +} from '@intra/shared-kernel'; +import { ReportAnalyticsDomain } from 'src/contexts/reporting/domain/report-analytics.domain'; +import { ReportInsightDomain } from 'src/contexts/reporting/domain/report-insight.domain'; +import { ReportDomain } from 'src/contexts/reporting/domain/report.domain'; +import { ReportHttpMapper } from 'src/contexts/reporting/presentation/http/mappers/report.http.mapper'; + +describe('ReportHttpMapper', () => { + describe('toResponse', () => { + it('produces summary/totals/insights blocks split by entity type', () => { + const questionAnalytic = ReportAnalyticsDomain.create({ + id: 1, + reportId: 10, + entityType: EntityType.QUESTION, + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + averageBySelfAssessment: 4, + averageByTeam: 3.5, + averageByOther: 4, + percentageBySelfAssessment: 80, + percentageByTeam: 70, + percentageByOther: 80, + deltaPercentageByTeam: -10, + deltaPercentageByOther: 0, + }); + const competenceAnalytic = ReportAnalyticsDomain.create({ + id: 2, + reportId: 10, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: 5, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4.5, + averageByTeam: 3.8, + averageByOther: 4.1, + percentageBySelfAssessment: 90, + percentageByTeam: 76, + percentageByOther: 82, + deltaPercentageByTeam: -14, + deltaPercentageByOther: -8, + }); + const questionInsight = ReportInsightDomain.create({ + id: 1, + reportId: 10, + insightType: InsightType.HIGHEST_RATING, + entityType: EntityType.QUESTION, + questionId: 100, + questionTitle: 'Q?', + competenceId: null, + competenceTitle: null, + averageScore: 4, + averageRating: 80, + averageDelta: -10, + }); + const competenceInsight = ReportInsightDomain.create({ + id: 2, + reportId: 10, + insightType: InsightType.LOWEST_RATING, + entityType: EntityType.COMPETENCE, + questionId: null, + questionTitle: null, + competenceId: 5, + competenceTitle: 'Teamwork', + averageScore: 3.8, + averageRating: 76, + averageDelta: -14, + }); + + const report = ReportDomain.create({ + id: 10, + reviewId: 1, + respondentCount: 5, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 20, + turnoutPctOfTeam: 75.5, + turnoutPctOfOther: 80, + questionTotAvgBySelf: 4, + questionTotAvgByTeam: 3.5, + questionTotAvgByOthers: 4, + questionTotPctBySelf: 80, + questionTotPctByTeam: 70, + questionTotPctByOthers: 80, + questionTotDeltaPctByTeam: -10, + questionTotDeltaPctByOthers: 0, + competenceTotAvgBySelf: 4.5, + competenceTotAvgByTeam: 3.8, + competenceTotAvgByOthers: 4.1, + competenceTotPctBySelf: 90, + competenceTotPctByTeam: 76, + competenceTotPctByOthers: 82, + competenceTotDeltaPctByTeam: -14, + competenceTotDeltaPctByOthers: -8, + analytics: [questionAnalytic, competenceAnalytic], + insights: [questionInsight, competenceInsight], + comments: [], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = ReportHttpMapper.toResponse(report); + + expect(response.id).toBe(10); + expect(response.reviewId).toBe(1); + expect(response.respondentCategories).toEqual([ + RespondentCategory.TEAM, + ]); + expect(response.turnoutPctOfTeam).toBe(75.5); + expect(response.questionSummaries).toHaveLength(1); + expect(response.questionSummaries[0].questionId).toBe(100); + expect(response.competenceSummaries).toHaveLength(1); + expect(response.competenceSummaries[0].competenceId).toBe(5); + expect(response.questionSummaryTotals).not.toBeNull(); + expect( + response.questionSummaryTotals!.averageBySelfAssessment, + ).toBe(4); + expect(response.competenceSummaryTotals).not.toBeNull(); + expect(response.questionInsights).toHaveLength(1); + expect(response.competenceInsights).toHaveLength(1); + expect(response.analytics).toHaveLength(2); + }); + + it('returns null totals blocks when every summary field is null', () => { + const report = ReportDomain.create({ + id: 1, + reviewId: 1, + respondentCount: 1, + respondentCategories: [RespondentCategory.TEAM], + answerCount: 1, + analytics: [], + }); + + const response = ReportHttpMapper.toResponse(report); + + expect(response.questionSummaryTotals).toBeNull(); + expect(response.competenceSummaryTotals).toBeNull(); + expect(response.questionSummaries).toEqual([]); + expect(response.competenceSummaries).toEqual([]); + }); + }); + + describe('toTextAnswerResponse', () => { + it('maps a ReportTextAnswerDto onto a TextAnswerResponse', () => { + const response = ReportHttpMapper.toTextAnswerResponse({ + questionId: 1, + questionTitle: 'Q?', + respondentCategory: RespondentCategory.TEAM, + textValue: 'Great teammate', + }); + + expect(response.questionId).toBe(1); + expect(response.questionTitle).toBe('Q?'); + expect(response.respondentCategory).toBe(RespondentCategory.TEAM); + expect(response.textValue).toBe('Great teammate'); + }); + + it('returns null questionTitle when none is provided', () => { + const response = ReportHttpMapper.toTextAnswerResponse({ + questionId: 1, + questionTitle: null, + respondentCategory: RespondentCategory.OTHER, + textValue: 'x', + }); + + expect(response.questionTitle).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report-analytics.http.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report-analytics.http.mapper.spec.ts new file mode 100644 index 00000000..f868b99c --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report-analytics.http.mapper.spec.ts @@ -0,0 +1,72 @@ +import Decimal from 'decimal.js'; +import { StrategicReportAnalyticsDomain } from 'src/contexts/reporting/domain/strategic-report-analytics.domain'; +import { StrategicReportAnalyticsHttpMapper } from 'src/contexts/reporting/presentation/http/mappers/strategic-report-analytics.http.mapper'; + +describe('StrategicReportAnalyticsHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field, rounding decimals to 4dp', () => { + const domain = StrategicReportAnalyticsDomain.create({ + id: 1, + strategicReportId: 10, + competenceId: 7, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4, + averageByTeam: 3.456789, + averageByOther: 3.9, + percentageBySelfAssessment: 80, + percentageByTeam: 69.5, + percentageByOther: 78, + deltaPercentageByTeam: -10, + deltaPercentageByOther: -2, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = + StrategicReportAnalyticsHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.strategicReportId).toBe(10); + expect(response.competenceId).toBe(7); + expect(response.competenceTitle).toBe('Teamwork'); + expect(response.averageByTeam).toBeCloseTo(3.4568, 4); + expect(response.percentageBySelfAssessment).toBe(80); + expect(response.deltaPercentageByTeam).toBe(-10); + expect(response.createdAt).toEqual( + new Date('2025-01-01T00:00:00.000Z'), + ); + }); + + it('returns null for missing decimal fields', () => { + const domain = StrategicReportAnalyticsDomain.create({ + strategicReportId: 10, + competenceId: 7, + competenceTitle: 'Teamwork', + }); + + const response = + StrategicReportAnalyticsHttpMapper.toResponse(domain); + + expect(response.averageBySelfAssessment).toBeNull(); + expect(response.deltaPercentageByOther).toBeNull(); + }); + }); + + describe('roundScore', () => { + it('rounds a Decimal value to 4 decimal places', () => { + expect( + StrategicReportAnalyticsHttpMapper.roundScore( + new Decimal('3.456789'), + ), + ).toBeCloseTo(3.4568, 4); + }); + + it('returns null for null and undefined', () => { + expect( + StrategicReportAnalyticsHttpMapper.roundScore(null), + ).toBeNull(); + expect( + StrategicReportAnalyticsHttpMapper.roundScore(undefined), + ).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report-insight.http.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report-insight.http.mapper.spec.ts new file mode 100644 index 00000000..9b2e2378 --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report-insight.http.mapper.spec.ts @@ -0,0 +1,69 @@ +import { InsightType } from '@intra/shared-kernel'; +import Decimal from 'decimal.js'; +import { StrategicReportInsightDomain } from 'src/contexts/reporting/domain/startegic-report-insight.domain'; +import { StrategicReportInsightHttpMapper } from 'src/contexts/reporting/presentation/http/mappers/startegic-report-insight.http.mapper'; + +describe('StrategicReportInsightHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field, rounding decimals to 4dp', () => { + const domain = StrategicReportInsightDomain.create({ + id: 1, + strategicReportId: 10, + insightType: InsightType.HIGHEST_RATING, + competenceId: 7, + competenceTitle: 'Teamwork', + averageScore: 4.5, + averageRating: 90, + averageDelta: -5, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = + StrategicReportInsightHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.strategicReportId).toBe(10); + expect(response.insightType).toBe(InsightType.HIGHEST_RATING); + expect(response.competenceId).toBe(7); + expect(response.competenceTitle).toBe('Teamwork'); + expect(response.averageScore).toBe(4.5); + expect(response.averageRating).toBe(90); + expect(response.averageDelta).toBe(-5); + }); + + it('returns null for missing decimal fields', () => { + const domain = StrategicReportInsightDomain.create({ + strategicReportId: 10, + insightType: InsightType.LOWEST_DELTA, + competenceId: 7, + competenceTitle: 'Teamwork', + }); + + const response = + StrategicReportInsightHttpMapper.toResponse(domain); + + expect(response.averageScore).toBeNull(); + expect(response.averageRating).toBeNull(); + expect(response.averageDelta).toBeNull(); + }); + }); + + describe('roundScore', () => { + it('rounds a Decimal value to 4 decimal places', () => { + expect( + StrategicReportInsightHttpMapper.roundScore( + new Decimal('3.456789'), + ), + ).toBeCloseTo(3.4568, 4); + }); + + it('returns null for null and undefined', () => { + expect( + StrategicReportInsightHttpMapper.roundScore(null), + ).toBeNull(); + expect( + StrategicReportInsightHttpMapper.roundScore(undefined), + ).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report.http.mapper.spec.ts b/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report.http.mapper.spec.ts new file mode 100644 index 00000000..962f2f6e --- /dev/null +++ b/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report.http.mapper.spec.ts @@ -0,0 +1,101 @@ +import { InsightType } from '@intra/shared-kernel'; +import { StrategicReportInsightDomain } from 'src/contexts/reporting/domain/startegic-report-insight.domain'; +import { StrategicReportAnalyticsDomain } from 'src/contexts/reporting/domain/strategic-report-analytics.domain'; +import { StrategicReportDomain } from 'src/contexts/reporting/domain/strategic-report.domain'; +import { StrategicReportHttpMapper } from 'src/contexts/reporting/presentation/http/mappers/strategic-report.http.mapper'; + +describe('StrategicReportHttpMapper', () => { + describe('toResponse', () => { + it('maps every domain field onto the response and includes relations', () => { + const analytics = StrategicReportAnalyticsDomain.create({ + strategicReportId: 1, + competenceId: 7, + competenceTitle: 'Teamwork', + averageBySelfAssessment: 4, + }); + const insight = StrategicReportInsightDomain.create({ + strategicReportId: 1, + insightType: InsightType.HIGHEST_RATING, + competenceId: 7, + competenceTitle: 'Teamwork', + averageScore: 4.5, + }); + + const domain = StrategicReportDomain.create({ + id: 1, + cycleId: 100, + cycleTitle: 'Q1', + rateeCount: 1, + rateeIds: [11], + respondentCount: 2, + respondentIds: [20, 21], + answerCount: 50, + reviewerCount: 1, + reviewerIds: [40], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 1, + competenceIds: [7], + questionCount: 0, + questionIds: [], + turnoutAvgPctOfRatees: 100, + turnoutAvgPctOfTeams: 90.5, + turnoutAvgPctOfOthers: 88, + competenceGeneralAvgSelf: 4, + analytics: [analytics], + insights: [insight], + createdAt: new Date('2025-01-01T00:00:00.000Z'), + }); + + const response = StrategicReportHttpMapper.toResponse(domain); + + expect(response.id).toBe(1); + expect(response.cycleId).toBe(100); + expect(response.cycleTitle).toBe('Q1'); + expect(response.rateeIds).toEqual([11]); + expect(response.respondentIds).toEqual([20, 21]); + expect(response.competenceIds).toEqual([7]); + expect(response.turnoutAvgPctOfRatees).toBe(100); + expect(response.turnoutAvgPctOfTeams).toBe(90.5); + expect(response.competenceGeneralAvgSelf).toBe(4); + expect(response.analytics).toHaveLength(1); + expect(response.analytics[0].competenceId).toBe(7); + expect(response.insights).toHaveLength(1); + expect(response.insights[0].insightType).toBe( + InsightType.HIGHEST_RATING, + ); + }); + + it('returns empty analytics/insights arrays when none are provided', () => { + const domain = StrategicReportDomain.create({ + id: 1, + cycleId: 100, + cycleTitle: 'Q1', + rateeCount: 0, + rateeIds: [], + respondentCount: 0, + respondentIds: [], + answerCount: 0, + reviewerCount: 0, + reviewerIds: [], + teamCount: 0, + teamIds: [], + positionCount: 0, + positionIds: [], + competenceCount: 0, + competenceIds: [], + questionCount: 0, + questionIds: [], + }); + + const response = StrategicReportHttpMapper.toResponse(domain); + + expect(response.analytics).toEqual([]); + expect(response.insights).toEqual([]); + expect(response.turnoutAvgPctOfRatees).toBeNull(); + expect(response.competenceGeneralAvgSelf).toBeNull(); + }); + }); +}); diff --git a/apps/api/test/unit/coverage/clover.xml b/apps/api/test/unit/coverage/clover.xml new file mode 100644 index 00000000..b8106ef5 --- /dev/null +++ b/apps/api/test/unit/coverage/clover.xml @@ -0,0 +1,2097 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/api/test/unit/coverage/coverage-final.json b/apps/api/test/unit/coverage/coverage-final.json new file mode 100644 index 00000000..b299200d --- /dev/null +++ b/apps/api/test/unit/coverage/coverage-final.json @@ -0,0 +1,70 @@ +{"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/constants/cycle-stage-transitions.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/constants/cycle-stage-transitions.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":50}},"1":{"start":{"line":7,"column":13},"end":{"line":21,"column":2}},"2":{"start":{"line":26,"column":13},"end":{"line":32,"column":11}}},"fnMap":{},"branchMap":{},"s":{"0":19,"1":19,"2":19},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/constants/response-status-transitions.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/constants/response-status-transitions.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":54}},"1":{"start":{"line":7,"column":13},"end":{"line":24,"column":2}},"2":{"start":{"line":29,"column":13},"end":{"line":34,"column":11}}},"fnMap":{},"branchMap":{},"s":{"0":8,"1":8,"2":8},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/constants/review-stage-transitions.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/constants/review-stage-transitions.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":51}},"1":{"start":{"line":7,"column":13},"end":{"line":46,"column":2}},"2":{"start":{"line":51,"column":13},"end":{"line":57,"column":11}}},"fnMap":{},"branchMap":{},"s":{"0":8,"1":8,"2":8},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/cycle-stage-changed.event.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/cycle-stage-changed.event.ts","statementMap":{"0":{"start":{"line":5,"column":24},"end":{"line":5,"column":39}},"1":{"start":{"line":6,"column":24},"end":{"line":6,"column":45}},"2":{"start":{"line":7,"column":24},"end":{"line":7,"column":43}},"3":{"start":{"line":8,"column":24},"end":{"line":8,"column":51}},"4":{"start":{"line":9,"column":24},"end":{"line":9,"column":53}},"5":{"start":{"line":10,"column":24},"end":{"line":10,"column":46}},"6":{"start":{"line":3,"column":0},"end":{"line":3,"column":13}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":4,"column":4},"end":{"line":4,"column":null}},"loc":{"start":{"line":10,"column":46},"end":{"line":11,"column":8}}}},"branchMap":{},"s":{"0":15,"1":15,"2":15,"3":15,"4":15,"5":15,"6":19},"f":{"0":15},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/cycle-stage-processed.event.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/cycle-stage-processed.event.ts","statementMap":{"0":{"start":{"line":6,"column":24},"end":{"line":6,"column":39}},"1":{"start":{"line":7,"column":24},"end":{"line":7,"column":48}},"2":{"start":{"line":8,"column":24},"end":{"line":8,"column":51}},"3":{"start":{"line":9,"column":24},"end":{"line":9,"column":53}},"4":{"start":{"line":10,"column":24},"end":{"line":10,"column":46}},"5":{"start":{"line":16,"column":8},"end":{"line":22,"column":10}},"6":{"start":{"line":4,"column":0},"end":{"line":4,"column":13}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":5,"column":4},"end":{"line":5,"column":null}},"loc":{"start":{"line":10,"column":46},"end":{"line":11,"column":8}}},"1":{"name":"(anonymous_1)","decl":{"start":{"line":13,"column":4},"end":{"line":13,"column":10}},"loc":{"start":{"line":14,"column":37},"end":{"line":23,"column":5}}}},"branchMap":{},"s":{"0":9,"1":9,"2":9,"3":9,"4":9,"5":6,"6":3},"f":{"0":9,"1":6},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/respondent-status-changed.event.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/respondent-status-changed.event.ts","statementMap":{"0":{"start":{"line":5,"column":24},"end":{"line":5,"column":40}},"1":{"start":{"line":6,"column":24},"end":{"line":6,"column":52}},"2":{"start":{"line":7,"column":24},"end":{"line":7,"column":50}},"3":{"start":{"line":8,"column":24},"end":{"line":8,"column":48}},"4":{"start":{"line":9,"column":24},"end":{"line":9,"column":51}},"5":{"start":{"line":10,"column":24},"end":{"line":10,"column":53}},"6":{"start":{"line":11,"column":24},"end":{"line":11,"column":46}},"7":{"start":{"line":3,"column":0},"end":{"line":3,"column":13}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":4,"column":4},"end":{"line":4,"column":null}},"loc":{"start":{"line":11,"column":46},"end":{"line":12,"column":8}}}},"branchMap":{},"s":{"0":12,"1":12,"2":12,"3":12,"4":12,"5":12,"6":12,"7":7},"f":{"0":12},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/review-stage-changed.event.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/review-stage-changed.event.ts","statementMap":{"0":{"start":{"line":5,"column":24},"end":{"line":5,"column":40}},"1":{"start":{"line":6,"column":24},"end":{"line":6,"column":46}},"2":{"start":{"line":7,"column":24},"end":{"line":7,"column":44}},"3":{"start":{"line":8,"column":24},"end":{"line":8,"column":51}},"4":{"start":{"line":9,"column":24},"end":{"line":9,"column":53}},"5":{"start":{"line":10,"column":24},"end":{"line":10,"column":46}},"6":{"start":{"line":3,"column":0},"end":{"line":3,"column":13}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":4,"column":4},"end":{"line":4,"column":null}},"loc":{"start":{"line":10,"column":46},"end":{"line":11,"column":8}}}},"branchMap":{},"s":{"0":18,"1":18,"2":18,"3":18,"4":18,"5":18,"6":10},"f":{"0":18},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/review-stage-processed.event.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/events/review-stage-processed.event.ts","statementMap":{"0":{"start":{"line":6,"column":24},"end":{"line":6,"column":40}},"1":{"start":{"line":7,"column":24},"end":{"line":7,"column":49}},"2":{"start":{"line":8,"column":24},"end":{"line":8,"column":51}},"3":{"start":{"line":9,"column":24},"end":{"line":9,"column":53}},"4":{"start":{"line":10,"column":24},"end":{"line":10,"column":46}},"5":{"start":{"line":16,"column":8},"end":{"line":22,"column":10}},"6":{"start":{"line":4,"column":0},"end":{"line":4,"column":13}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":5,"column":4},"end":{"line":5,"column":null}},"loc":{"start":{"line":10,"column":46},"end":{"line":11,"column":8}}},"1":{"name":"(anonymous_1)","decl":{"start":{"line":13,"column":4},"end":{"line":13,"column":10}},"loc":{"start":{"line":14,"column":38},"end":{"line":23,"column":5}}}},"branchMap":{},"s":{"0":11,"1":11,"2":11,"3":11,"4":11,"5":8,"6":3},"f":{"0":11,"1":8},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/cycle-stage.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/cycle-stage.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":50}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":63}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":107}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":121}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":81}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"7":{"start":{"line":18,"column":31},"end":{"line":85,"column":null}},"8":{"start":{"line":19,"column":30},"end":{"line":19,"column":65}},"9":{"start":{"line":22,"column":25},"end":{"line":22,"column":36}},"10":{"start":{"line":23,"column":25},"end":{"line":23,"column":39}},"11":{"start":{"line":25,"column":25},"end":{"line":25,"column":34}},"12":{"start":{"line":37,"column":8},"end":{"line":39,"column":10}},"13":{"start":{"line":41,"column":8},"end":{"line":48,"column":9}},"14":{"start":{"line":43,"column":12},"end":{"line":46,"column":14}},"15":{"start":{"line":47,"column":12},"end":{"line":47,"column":19}},"16":{"start":{"line":50,"column":24},"end":{"line":50,"column":71}},"17":{"start":{"line":52,"column":8},"end":{"line":61,"column":9}},"18":{"start":{"line":53,"column":12},"end":{"line":55,"column":14}},"19":{"start":{"line":56,"column":12},"end":{"line":59,"column":14}},"20":{"start":{"line":60,"column":12},"end":{"line":60,"column":19}},"21":{"start":{"line":63,"column":8},"end":{"line":65,"column":10}},"22":{"start":{"line":67,"column":8},"end":{"line":83,"column":9}},"23":{"start":{"line":68,"column":12},"end":{"line":68,"column":74}},"24":{"start":{"line":70,"column":12},"end":{"line":72,"column":14}},"25":{"start":{"line":74,"column":12},"end":{"line":76,"column":14}},"26":{"start":{"line":79,"column":12},"end":{"line":82,"column":14}},"27":{"start":{"line":18,"column":13},"end":{"line":18,"column":31}},"28":{"start":{"line":34,"column":10},"end":{"line":84,"column":null}},"29":{"start":{"line":18,"column":13},"end":{"line":85,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"loc":{"start":{"line":25,"column":54},"end":{"line":26,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":34,"column":10},"end":{"line":34,"column":33}},"loc":{"start":{"line":35,"column":37},"end":{"line":84,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":41,"column":8},"end":{"line":48,"column":9}},"type":"if","locations":[{"start":{"line":41,"column":8},"end":{"line":48,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":52,"column":8},"end":{"line":61,"column":9}},"type":"if","locations":[{"start":{"line":52,"column":8},"end":{"line":61,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":35,"column":15},"end":{"line":35,"column":null}},"type":"cond-expr","locations":[{"start":{"line":35,"column":37},"end":{"line":35,"column":null}},{"start":{"line":35,"column":37},"end":{"line":35,"column":null}}]},"3":{"loc":{"start":{"line":35,"column":15},"end":{"line":35,"column":37}},"type":"binary-expr","locations":[{"start":{"line":35,"column":15},"end":{"line":35,"column":null}},{"start":{"line":35,"column":15},"end":{"line":35,"column":37}}]},"4":{"loc":{"start":{"line":36,"column":7},"end":{"line":36,"column":null}},"type":"cond-expr","locations":[{"start":{"line":36,"column":14},"end":{"line":36,"column":null}},{"start":{"line":36,"column":14},"end":{"line":36,"column":null}}]},"5":{"loc":{"start":{"line":36,"column":7},"end":{"line":36,"column":14}},"type":"binary-expr","locations":[{"start":{"line":36,"column":7},"end":{"line":36,"column":null}},{"start":{"line":36,"column":7},"end":{"line":36,"column":14}}]},"6":{"loc":{"start":{"line":22,"column":36},"end":{"line":22,"column":null}},"type":"cond-expr","locations":[{"start":{"line":22,"column":64},"end":{"line":22,"column":null}},{"start":{"line":22,"column":64},"end":{"line":22,"column":null}}]},"7":{"loc":{"start":{"line":22,"column":36},"end":{"line":22,"column":64}},"type":"binary-expr","locations":[{"start":{"line":22,"column":36},"end":{"line":22,"column":null}},{"start":{"line":22,"column":36},"end":{"line":22,"column":64}}]},"8":{"loc":{"start":{"line":22,"column":64},"end":{"line":23,"column":null}},"type":"cond-expr","locations":[{"start":{"line":23,"column":52},"end":{"line":23,"column":null}},{"start":{"line":23,"column":52},"end":{"line":23,"column":null}}]},"9":{"loc":{"start":{"line":22,"column":64},"end":{"line":23,"column":52}},"type":"binary-expr","locations":[{"start":{"line":22,"column":64},"end":{"line":23,"column":null}},{"start":{"line":23,"column":39},"end":{"line":23,"column":52}}]},"10":{"loc":{"start":{"line":23,"column":52},"end":{"line":25,"column":null}},"type":"cond-expr","locations":[{"start":{"line":25,"column":54},"end":{"line":25,"column":null}},{"start":{"line":25,"column":54},"end":{"line":25,"column":null}}]},"11":{"loc":{"start":{"line":23,"column":52},"end":{"line":25,"column":54}},"type":"binary-expr","locations":[{"start":{"line":23,"column":52},"end":{"line":25,"column":null}},{"start":{"line":25,"column":34},"end":{"line":25,"column":54}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":4,"9":4,"10":4,"11":4,"12":4,"13":4,"14":1,"15":1,"16":3,"17":3,"18":1,"19":1,"20":1,"21":2,"22":2,"23":2,"24":1,"25":1,"26":2,"27":1,"28":1,"29":1},"f":{"0":4,"1":4},"b":{"0":[1,3],"1":[1,2],"2":[1,0],"3":[1,1],"4":[1,0],"5":[1,1],"6":[1,0],"7":[1,1],"8":[1,0],"9":[1,1],"10":[0,1],"11":[1,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/respondent-status.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/respondent-status.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":6,"column":0},"end":{"line":6,"column":60}},"2":{"start":{"line":7,"column":0},"end":{"line":7,"column":63}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":75}},"4":{"start":{"line":9,"column":0},"end":{"line":9,"column":89}},"5":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"6":{"start":{"line":14,"column":0},"end":{"line":14,"column":null}},"7":{"start":{"line":18,"column":0},"end":{"line":18,"column":59}},"8":{"start":{"line":25,"column":37},"end":{"line":95,"column":null}},"9":{"start":{"line":26,"column":30},"end":{"line":26,"column":71}},"10":{"start":{"line":30,"column":25},"end":{"line":30,"column":38}},"11":{"start":{"line":31,"column":25},"end":{"line":31,"column":34}},"12":{"start":{"line":32,"column":25},"end":{"line":32,"column":39}},"13":{"start":{"line":44,"column":8},"end":{"line":46,"column":10}},"14":{"start":{"line":48,"column":8},"end":{"line":53,"column":9}},"15":{"start":{"line":52,"column":12},"end":{"line":52,"column":19}},"16":{"start":{"line":55,"column":8},"end":{"line":57,"column":10}},"17":{"start":{"line":59,"column":8},"end":{"line":93,"column":9}},"18":{"start":{"line":60,"column":32},"end":{"line":62,"column":null}},"19":{"start":{"line":64,"column":33},"end":{"line":67,"column":null}},"20":{"start":{"line":66,"column":20},"end":{"line":67,"column":73}},"21":{"start":{"line":70,"column":12},"end":{"line":87,"column":13}},"22":{"start":{"line":71,"column":16},"end":{"line":71,"column":66}},"23":{"start":{"line":72,"column":16},"end":{"line":74,"column":18}},"24":{"start":{"line":76,"column":16},"end":{"line":86,"column":18}},"25":{"start":{"line":80,"column":32},"end":{"line":83,"column":62}},"26":{"start":{"line":89,"column":12},"end":{"line":92,"column":14}},"27":{"start":{"line":25,"column":13},"end":{"line":25,"column":37}},"28":{"start":{"line":41,"column":10},"end":{"line":94,"column":null}},"29":{"start":{"line":25,"column":13},"end":{"line":95,"column":null}},"30":{"start":{"line":106,"column":44},"end":{"line":166,"column":null}},"31":{"start":{"line":107,"column":30},"end":{"line":107,"column":78}},"32":{"start":{"line":111,"column":25},"end":{"line":111,"column":38}},"33":{"start":{"line":113,"column":25},"end":{"line":113,"column":34}},"34":{"start":{"line":114,"column":25},"end":{"line":114,"column":40}},"35":{"start":{"line":121,"column":8},"end":{"line":123,"column":9}},"36":{"start":{"line":122,"column":12},"end":{"line":122,"column":19}},"37":{"start":{"line":125,"column":8},"end":{"line":164,"column":9}},"38":{"start":{"line":126,"column":31},"end":{"line":127,"column":null}},"39":{"start":{"line":130,"column":12},"end":{"line":132,"column":13}},"40":{"start":{"line":131,"column":16},"end":{"line":131,"column":23}},"41":{"start":{"line":134,"column":27},"end":{"line":134,"column":70}},"42":{"start":{"line":135,"column":12},"end":{"line":140,"column":13}},"43":{"start":{"line":136,"column":16},"end":{"line":138,"column":18}},"44":{"start":{"line":139,"column":16},"end":{"line":139,"column":23}},"45":{"start":{"line":142,"column":12},"end":{"line":144,"column":13}},"46":{"start":{"line":143,"column":16},"end":{"line":143,"column":23}},"47":{"start":{"line":146,"column":12},"end":{"line":148,"column":14}},"48":{"start":{"line":150,"column":12},"end":{"line":156,"column":14}},"49":{"start":{"line":158,"column":12},"end":{"line":163,"column":14}},"50":{"start":{"line":106,"column":13},"end":{"line":106,"column":44}},"51":{"start":{"line":118,"column":10},"end":{"line":165,"column":null}},"52":{"start":{"line":106,"column":13},"end":{"line":166,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"loc":{"start":{"line":32,"column":52},"end":{"line":33,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":41,"column":10},"end":{"line":41,"column":39}},"loc":{"start":{"line":42,"column":43},"end":{"line":94,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":65,"column":16},"end":{"line":65,"column":17}},"loc":{"start":{"line":66,"column":20},"end":{"line":67,"column":73}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":79,"column":28},"end":{"line":79,"column":29}},"loc":{"start":{"line":80,"column":32},"end":{"line":83,"column":62}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":109,"column":4},"end":{"line":109,"column":null}},"loc":{"start":{"line":114,"column":53},"end":{"line":115,"column":8}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":118,"column":10},"end":{"line":118,"column":35}},"loc":{"start":{"line":119,"column":43},"end":{"line":165,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":48,"column":8},"end":{"line":53,"column":9}},"type":"if","locations":[{"start":{"line":48,"column":8},"end":{"line":53,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":49,"column":12},"end":{"line":50,"column":54}},"type":"binary-expr","locations":[{"start":{"line":49,"column":12},"end":{"line":49,"column":55}},{"start":{"line":50,"column":12},"end":{"line":50,"column":54}}]},"2":{"loc":{"start":{"line":66,"column":20},"end":{"line":67,"column":73}},"type":"binary-expr","locations":[{"start":{"line":66,"column":20},"end":{"line":66,"column":74}},{"start":{"line":67,"column":20},"end":{"line":67,"column":73}}]},"3":{"loc":{"start":{"line":70,"column":12},"end":{"line":87,"column":13}},"type":"if","locations":[{"start":{"line":70,"column":12},"end":{"line":87,"column":13}},{"start":{"line":75,"column":19},"end":{"line":87,"column":13}}]},"4":{"loc":{"start":{"line":80,"column":32},"end":{"line":83,"column":62}},"type":"binary-expr","locations":[{"start":{"line":80,"column":32},"end":{"line":81,"column":58}},{"start":{"line":82,"column":32},"end":{"line":83,"column":62}}]},"5":{"loc":{"start":{"line":42,"column":15},"end":{"line":42,"column":null}},"type":"cond-expr","locations":[{"start":{"line":42,"column":43},"end":{"line":42,"column":null}},{"start":{"line":42,"column":43},"end":{"line":42,"column":null}}]},"6":{"loc":{"start":{"line":42,"column":15},"end":{"line":42,"column":43}},"type":"binary-expr","locations":[{"start":{"line":42,"column":15},"end":{"line":42,"column":null}},{"start":{"line":42,"column":15},"end":{"line":42,"column":43}}]},"7":{"loc":{"start":{"line":43,"column":7},"end":{"line":43,"column":null}},"type":"cond-expr","locations":[{"start":{"line":43,"column":14},"end":{"line":43,"column":null}},{"start":{"line":43,"column":14},"end":{"line":43,"column":null}}]},"8":{"loc":{"start":{"line":43,"column":7},"end":{"line":43,"column":14}},"type":"binary-expr","locations":[{"start":{"line":43,"column":7},"end":{"line":43,"column":null}},{"start":{"line":43,"column":7},"end":{"line":43,"column":14}}]},"9":{"loc":{"start":{"line":30,"column":38},"end":{"line":30,"column":null}},"type":"cond-expr","locations":[{"start":{"line":30,"column":62},"end":{"line":30,"column":null}},{"start":{"line":30,"column":62},"end":{"line":30,"column":null}}]},"10":{"loc":{"start":{"line":30,"column":38},"end":{"line":30,"column":62}},"type":"binary-expr","locations":[{"start":{"line":30,"column":38},"end":{"line":30,"column":null}},{"start":{"line":30,"column":38},"end":{"line":30,"column":62}}]},"11":{"loc":{"start":{"line":30,"column":62},"end":{"line":31,"column":null}},"type":"cond-expr","locations":[{"start":{"line":31,"column":47},"end":{"line":31,"column":null}},{"start":{"line":31,"column":47},"end":{"line":31,"column":null}}]},"12":{"loc":{"start":{"line":30,"column":62},"end":{"line":31,"column":47}},"type":"binary-expr","locations":[{"start":{"line":30,"column":62},"end":{"line":31,"column":null}},{"start":{"line":31,"column":34},"end":{"line":31,"column":47}}]},"13":{"loc":{"start":{"line":31,"column":47},"end":{"line":32,"column":null}},"type":"cond-expr","locations":[{"start":{"line":32,"column":52},"end":{"line":32,"column":null}},{"start":{"line":32,"column":52},"end":{"line":32,"column":null}}]},"14":{"loc":{"start":{"line":31,"column":47},"end":{"line":32,"column":52}},"type":"binary-expr","locations":[{"start":{"line":31,"column":47},"end":{"line":32,"column":null}},{"start":{"line":32,"column":39},"end":{"line":32,"column":52}}]},"15":{"loc":{"start":{"line":121,"column":8},"end":{"line":123,"column":9}},"type":"if","locations":[{"start":{"line":121,"column":8},"end":{"line":123,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":130,"column":12},"end":{"line":132,"column":13}},"type":"if","locations":[{"start":{"line":130,"column":12},"end":{"line":132,"column":13}},{"start":{},"end":{}}]},"17":{"loc":{"start":{"line":135,"column":12},"end":{"line":140,"column":13}},"type":"if","locations":[{"start":{"line":135,"column":12},"end":{"line":140,"column":13}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":142,"column":12},"end":{"line":144,"column":13}},"type":"if","locations":[{"start":{"line":142,"column":12},"end":{"line":144,"column":13}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":153,"column":16},"end":{"line":153,"column":46}},"type":"binary-expr","locations":[{"start":{"line":153,"column":16},"end":{"line":153,"column":33}},{"start":{"line":153,"column":37},"end":{"line":153,"column":46}}]},"20":{"loc":{"start":{"line":154,"column":16},"end":{"line":154,"column":48}},"type":"binary-expr","locations":[{"start":{"line":154,"column":16},"end":{"line":154,"column":35}},{"start":{"line":154,"column":39},"end":{"line":154,"column":48}}]},"21":{"loc":{"start":{"line":119,"column":15},"end":{"line":119,"column":null}},"type":"cond-expr","locations":[{"start":{"line":119,"column":43},"end":{"line":119,"column":null}},{"start":{"line":119,"column":43},"end":{"line":119,"column":null}}]},"22":{"loc":{"start":{"line":119,"column":15},"end":{"line":119,"column":43}},"type":"binary-expr","locations":[{"start":{"line":119,"column":15},"end":{"line":119,"column":null}},{"start":{"line":119,"column":15},"end":{"line":119,"column":43}}]},"23":{"loc":{"start":{"line":120,"column":7},"end":{"line":120,"column":null}},"type":"cond-expr","locations":[{"start":{"line":120,"column":14},"end":{"line":120,"column":null}},{"start":{"line":120,"column":14},"end":{"line":120,"column":null}}]},"24":{"loc":{"start":{"line":120,"column":7},"end":{"line":120,"column":14}},"type":"binary-expr","locations":[{"start":{"line":120,"column":7},"end":{"line":120,"column":null}},{"start":{"line":120,"column":7},"end":{"line":120,"column":14}}]},"25":{"loc":{"start":{"line":111,"column":38},"end":{"line":111,"column":null}},"type":"cond-expr","locations":[{"start":{"line":111,"column":62},"end":{"line":111,"column":null}},{"start":{"line":111,"column":62},"end":{"line":111,"column":null}}]},"26":{"loc":{"start":{"line":111,"column":38},"end":{"line":111,"column":62}},"type":"binary-expr","locations":[{"start":{"line":111,"column":38},"end":{"line":111,"column":null}},{"start":{"line":111,"column":38},"end":{"line":111,"column":62}}]},"27":{"loc":{"start":{"line":111,"column":62},"end":{"line":113,"column":null}},"type":"cond-expr","locations":[{"start":{"line":113,"column":54},"end":{"line":113,"column":null}},{"start":{"line":113,"column":54},"end":{"line":113,"column":null}}]},"28":{"loc":{"start":{"line":111,"column":62},"end":{"line":113,"column":54}},"type":"binary-expr","locations":[{"start":{"line":111,"column":62},"end":{"line":113,"column":null}},{"start":{"line":113,"column":34},"end":{"line":113,"column":54}}]},"29":{"loc":{"start":{"line":113,"column":54},"end":{"line":114,"column":null}},"type":"cond-expr","locations":[{"start":{"line":114,"column":53},"end":{"line":114,"column":null}},{"start":{"line":114,"column":53},"end":{"line":114,"column":null}}]},"30":{"loc":{"start":{"line":113,"column":54},"end":{"line":114,"column":53}},"type":"binary-expr","locations":[{"start":{"line":113,"column":54},"end":{"line":114,"column":null}},{"start":{"line":114,"column":40},"end":{"line":114,"column":53}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":4,"10":4,"11":4,"12":4,"13":4,"14":4,"15":1,"16":3,"17":3,"18":3,"19":2,"20":4,"21":2,"22":1,"23":1,"24":1,"25":2,"26":1,"27":1,"28":1,"29":1,"30":1,"31":6,"32":6,"33":6,"34":6,"35":6,"36":1,"37":5,"38":5,"39":5,"40":1,"41":4,"42":4,"43":1,"44":1,"45":3,"46":1,"47":2,"48":2,"49":1,"50":1,"51":1,"52":1},"f":{"0":4,"1":4,"2":4,"3":2,"4":6,"5":6},"b":{"0":[1,3],"1":[4,1],"2":[4,2],"3":[1,1],"4":[2,1],"5":[1,0],"6":[1,1],"7":[1,0],"8":[1,1],"9":[0,1],"10":[1,0],"11":[1,0],"12":[1,1],"13":[1,0],"14":[1,1],"15":[1,5],"16":[1,4],"17":[1,3],"18":[1,2],"19":[2,1],"20":[2,1],"21":[1,0],"22":[1,1],"23":[1,0],"24":[1,1],"25":[0,1],"26":[1,0],"27":[0,1],"28":[1,0],"29":[1,0],"30":[1,1]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/review-stage.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/listeners/review-stage.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":51}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":63}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":109}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":87}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":83}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"7":{"start":{"line":20,"column":53},"end":{"line":25,"column":2}},"8":{"start":{"line":32,"column":32},"end":{"line":125,"column":null}},"9":{"start":{"line":33,"column":30},"end":{"line":33,"column":66}},"10":{"start":{"line":36,"column":25},"end":{"line":36,"column":33}},"11":{"start":{"line":38,"column":25},"end":{"line":38,"column":34}},"12":{"start":{"line":39,"column":25},"end":{"line":39,"column":39}},"13":{"start":{"line":51,"column":8},"end":{"line":53,"column":10}},"14":{"start":{"line":55,"column":8},"end":{"line":123,"column":9}},"15":{"start":{"line":56,"column":12},"end":{"line":58,"column":13}},"16":{"start":{"line":57,"column":16},"end":{"line":57,"column":23}},"17":{"start":{"line":60,"column":27},"end":{"line":60,"column":70}},"18":{"start":{"line":62,"column":12},"end":{"line":65,"column":13}},"19":{"start":{"line":63,"column":16},"end":{"line":63,"column":71}},"20":{"start":{"line":64,"column":16},"end":{"line":64,"column":23}},"21":{"start":{"line":67,"column":12},"end":{"line":72,"column":13}},"22":{"start":{"line":68,"column":16},"end":{"line":70,"column":18}},"23":{"start":{"line":71,"column":16},"end":{"line":71,"column":23}},"24":{"start":{"line":74,"column":12},"end":{"line":76,"column":14}},"25":{"start":{"line":78,"column":12},"end":{"line":112,"column":13}},"26":{"start":{"line":79,"column":37},"end":{"line":80,"column":null}},"27":{"start":{"line":92,"column":40},"end":{"line":93,"column":null}},"28":{"start":{"line":93,"column":20},"end":{"line":93,"column":68}},"29":{"start":{"line":95,"column":37},"end":{"line":95,"column":65}},"30":{"start":{"line":97,"column":16},"end":{"line":106,"column":17}},"31":{"start":{"line":98,"column":20},"end":{"line":98,"column":68}},"32":{"start":{"line":99,"column":20},"end":{"line":101,"column":22}},"33":{"start":{"line":103,"column":20},"end":{"line":105,"column":22}},"34":{"start":{"line":108,"column":16},"end":{"line":111,"column":18}},"35":{"start":{"line":119,"column":12},"end":{"line":122,"column":14}},"36":{"start":{"line":32,"column":13},"end":{"line":32,"column":32}},"37":{"start":{"line":48,"column":10},"end":{"line":124,"column":null}},"38":{"start":{"line":32,"column":13},"end":{"line":125,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},"loc":{"start":{"line":39,"column":52},"end":{"line":40,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":48,"column":10},"end":{"line":48,"column":34}},"loc":{"start":{"line":49,"column":38},"end":{"line":124,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":92,"column":60},"end":{"line":92,"column":61}},"loc":{"start":{"line":93,"column":20},"end":{"line":93,"column":68}}}},"branchMap":{"0":{"loc":{"start":{"line":56,"column":12},"end":{"line":58,"column":13}},"type":"if","locations":[{"start":{"line":56,"column":12},"end":{"line":58,"column":13}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":62,"column":12},"end":{"line":65,"column":13}},"type":"if","locations":[{"start":{"line":62,"column":12},"end":{"line":65,"column":13}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":67,"column":12},"end":{"line":72,"column":13}},"type":"if","locations":[{"start":{"line":67,"column":12},"end":{"line":72,"column":13}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":97,"column":16},"end":{"line":106,"column":17}},"type":"if","locations":[{"start":{"line":97,"column":16},"end":{"line":106,"column":17}},{"start":{"line":102,"column":23},"end":{"line":106,"column":17}}]},"4":{"loc":{"start":{"line":49,"column":15},"end":{"line":49,"column":null}},"type":"cond-expr","locations":[{"start":{"line":49,"column":38},"end":{"line":49,"column":null}},{"start":{"line":49,"column":38},"end":{"line":49,"column":null}}]},"5":{"loc":{"start":{"line":49,"column":15},"end":{"line":49,"column":38}},"type":"binary-expr","locations":[{"start":{"line":49,"column":15},"end":{"line":49,"column":null}},{"start":{"line":49,"column":15},"end":{"line":49,"column":38}}]},"6":{"loc":{"start":{"line":50,"column":7},"end":{"line":50,"column":null}},"type":"cond-expr","locations":[{"start":{"line":50,"column":14},"end":{"line":50,"column":null}},{"start":{"line":50,"column":14},"end":{"line":50,"column":null}}]},"7":{"loc":{"start":{"line":50,"column":7},"end":{"line":50,"column":14}},"type":"binary-expr","locations":[{"start":{"line":50,"column":7},"end":{"line":50,"column":null}},{"start":{"line":50,"column":7},"end":{"line":50,"column":14}}]},"8":{"loc":{"start":{"line":36,"column":33},"end":{"line":36,"column":null}},"type":"cond-expr","locations":[{"start":{"line":36,"column":45},"end":{"line":36,"column":null}},{"start":{"line":36,"column":45},"end":{"line":36,"column":null}}]},"9":{"loc":{"start":{"line":36,"column":33},"end":{"line":36,"column":45}},"type":"binary-expr","locations":[{"start":{"line":36,"column":33},"end":{"line":36,"column":null}},{"start":{"line":36,"column":33},"end":{"line":36,"column":45}}]},"10":{"loc":{"start":{"line":36,"column":45},"end":{"line":38,"column":null}},"type":"cond-expr","locations":[{"start":{"line":38,"column":54},"end":{"line":38,"column":null}},{"start":{"line":38,"column":54},"end":{"line":38,"column":null}}]},"11":{"loc":{"start":{"line":36,"column":45},"end":{"line":38,"column":54}},"type":"binary-expr","locations":[{"start":{"line":36,"column":45},"end":{"line":38,"column":null}},{"start":{"line":38,"column":34},"end":{"line":38,"column":54}}]},"12":{"loc":{"start":{"line":38,"column":54},"end":{"line":39,"column":null}},"type":"cond-expr","locations":[{"start":{"line":39,"column":52},"end":{"line":39,"column":null}},{"start":{"line":39,"column":52},"end":{"line":39,"column":null}}]},"13":{"loc":{"start":{"line":38,"column":54},"end":{"line":39,"column":52}},"type":"binary-expr","locations":[{"start":{"line":38,"column":54},"end":{"line":39,"column":null}},{"start":{"line":39,"column":39},"end":{"line":39,"column":52}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":6,"10":6,"11":6,"12":6,"13":6,"14":6,"15":6,"16":1,"17":5,"18":5,"19":1,"20":1,"21":4,"22":1,"23":1,"24":3,"25":3,"26":3,"27":3,"28":6,"29":3,"30":3,"31":2,"32":1,"33":1,"34":1,"35":6,"36":1,"37":1,"38":1},"f":{"0":6,"1":6,"2":6},"b":{"0":[1,5],"1":[1,4],"2":[1,3],"3":[2,1],"4":[1,0],"5":[1,1],"6":[1,0],"7":[1,1],"8":[1,0],"9":[1,1],"10":[0,1],"11":[1,0],"12":[1,0],"13":[1,1]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/answer.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/answer.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":4,"column":73}}},"fnMap":{},"branchMap":{},"s":{"0":18},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/cluster-score-analytics.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/cluster-score-analytics.repository.port.ts","statementMap":{"0":{"start":{"line":7,"column":13},"end":{"line":9,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":3},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/cluster-score.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/cluster-score.repository.port.ts","statementMap":{"0":{"start":{"line":5,"column":13},"end":{"line":7,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":15},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/cycle-stage-history.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/cycle-stage-history.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":18},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/cycle.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/cycle.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":4,"column":71}}},"fnMap":{},"branchMap":{},"s":{"0":27},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/question.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/question.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":4,"column":77}}},"fnMap":{},"branchMap":{},"s":{"0":14},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/respondent.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/respondent.repository.port.ts","statementMap":{"0":{"start":{"line":7,"column":13},"end":{"line":9,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":21},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/review-question-relation.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/review-question-relation.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":6,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":13},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/review-stage-history.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/review-stage-history.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":7},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/review.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/review.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":4,"column":73}}},"fnMap":{},"branchMap":{},"s":{"0":27},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/reviewer.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/ports/reviewer.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":4,"column":77}}},"fnMap":{},"branchMap":{},"s":{"0":21},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/cluster-score-analytics.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/cluster-score-analytics.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"2":{"start":{"line":13,"column":0},"end":{"line":13,"column":33}},"3":{"start":{"line":14,"column":0},"end":{"line":14,"column":87}},"4":{"start":{"line":15,"column":0},"end":{"line":15,"column":90}},"5":{"start":{"line":16,"column":0},"end":{"line":16,"column":null}},"6":{"start":{"line":20,"column":0},"end":{"line":20,"column":null}},"7":{"start":{"line":24,"column":0},"end":{"line":24,"column":47}},"8":{"start":{"line":27,"column":7},"end":{"line":251,"column":null}},"9":{"start":{"line":30,"column":25},"end":{"line":30,"column":36}},"10":{"start":{"line":32,"column":25},"end":{"line":32,"column":40}},"11":{"start":{"line":33,"column":25},"end":{"line":33,"column":33}},"12":{"start":{"line":34,"column":25},"end":{"line":34,"column":35}},"13":{"start":{"line":40,"column":8},"end":{"line":40,"column":51}},"14":{"start":{"line":41,"column":25},"end":{"line":41,"column":54}},"15":{"start":{"line":42,"column":25},"end":{"line":42,"column":54}},"16":{"start":{"line":43,"column":29},"end":{"line":43,"column":62}},"17":{"start":{"line":44,"column":27},"end":{"line":44,"column":58}},"18":{"start":{"line":45,"column":27},"end":{"line":45,"column":58}},"19":{"start":{"line":47,"column":8},"end":{"line":47,"column":58}},"20":{"start":{"line":48,"column":8},"end":{"line":48,"column":68}},"21":{"start":{"line":50,"column":23},"end":{"line":60,"column":10}},"22":{"start":{"line":62,"column":8},"end":{"line":62,"column":45}},"23":{"start":{"line":68,"column":8},"end":{"line":68,"column":44}},"24":{"start":{"line":72,"column":21},"end":{"line":72,"column":54}},"25":{"start":{"line":73,"column":8},"end":{"line":76,"column":14}},"26":{"start":{"line":74,"column":12},"end":{"line":76,"column":14}},"27":{"start":{"line":77,"column":8},"end":{"line":77,"column":20}},"28":{"start":{"line":84,"column":24},"end":{"line":84,"column":46}},"29":{"start":{"line":87,"column":12},"end":{"line":89,"column":34}},"30":{"start":{"line":91,"column":12},"end":{"line":93,"column":34}},"31":{"start":{"line":95,"column":12},"end":{"line":97,"column":38}},"32":{"start":{"line":99,"column":8},"end":{"line":99,"column":68}},"33":{"start":{"line":101,"column":60},"end":{"line":117,"column":10}},"34":{"start":{"line":119,"column":8},"end":{"line":119,"column":54}},"35":{"start":{"line":123,"column":8},"end":{"line":123,"column":31}},"36":{"start":{"line":124,"column":8},"end":{"line":124,"column":44}},"37":{"start":{"line":130,"column":8},"end":{"line":130,"column":52}},"38":{"start":{"line":138,"column":8},"end":{"line":138,"column":43}},"39":{"start":{"line":140,"column":25},"end":{"line":140,"column":55}},"40":{"start":{"line":141,"column":30},"end":{"line":143,"column":10}},"41":{"start":{"line":145,"column":8},"end":{"line":200,"column":9}},"42":{"start":{"line":146,"column":12},"end":{"line":148,"column":13}},"43":{"start":{"line":147,"column":16},"end":{"line":147,"column":25}},"44":{"start":{"line":150,"column":41},"end":{"line":151,"column":null}},"45":{"start":{"line":151,"column":24},"end":{"line":151,"column":51}},"46":{"start":{"line":154,"column":12},"end":{"line":156,"column":13}},"47":{"start":{"line":155,"column":16},"end":{"line":155,"column":25}},"48":{"start":{"line":158,"column":27},"end":{"line":159,"column":null}},"49":{"start":{"line":159,"column":24},"end":{"line":159,"column":45}},"50":{"start":{"line":161,"column":29},"end":{"line":161,"column":70}},"51":{"start":{"line":162,"column":29},"end":{"line":162,"column":70}},"52":{"start":{"line":163,"column":33},"end":{"line":166,"column":35}},"53":{"start":{"line":164,"column":34},"end":{"line":164,"column":43}},"54":{"start":{"line":167,"column":35},"end":{"line":167,"column":62}},"55":{"start":{"line":169,"column":41},"end":{"line":171,"column":33}},"56":{"start":{"line":170,"column":31},"end":{"line":170,"column":70}},"57":{"start":{"line":171,"column":28},"end":{"line":171,"column":32}},"58":{"start":{"line":172,"column":39},"end":{"line":175,"column":null}},"59":{"start":{"line":174,"column":36},"end":{"line":174,"column":79}},"60":{"start":{"line":175,"column":33},"end":{"line":175,"column":43}},"61":{"start":{"line":177,"column":36},"end":{"line":179,"column":35}},"62":{"start":{"line":181,"column":31},"end":{"line":181,"column":62}},"63":{"start":{"line":182,"column":31},"end":{"line":182,"column":62}},"64":{"start":{"line":184,"column":12},"end":{"line":184,"column":62}},"65":{"start":{"line":185,"column":12},"end":{"line":185,"column":72}},"66":{"start":{"line":187,"column":27},"end":{"line":197,"column":14}},"67":{"start":{"line":199,"column":12},"end":{"line":199,"column":48}},"68":{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},"69":{"start":{"line":215,"column":12},"end":{"line":217,"column":14}},"70":{"start":{"line":219,"column":8},"end":{"line":223,"column":9}},"71":{"start":{"line":220,"column":12},"end":{"line":222,"column":14}},"72":{"start":{"line":235,"column":8},"end":{"line":239,"column":9}},"73":{"start":{"line":236,"column":12},"end":{"line":238,"column":14}},"74":{"start":{"line":240,"column":8},"end":{"line":244,"column":9}},"75":{"start":{"line":241,"column":12},"end":{"line":243,"column":14}},"76":{"start":{"line":245,"column":8},"end":{"line":249,"column":9}},"77":{"start":{"line":246,"column":12},"end":{"line":248,"column":14}},"78":{"start":{"line":27,"column":13},"end":{"line":27,"column":41}},"79":{"start":{"line":27,"column":13},"end":{"line":251,"column":null}}},"fnMap":{"0":{"name":"(anonymous_5)","decl":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"loc":{"start":{"line":34,"column":49},"end":{"line":35,"column":8}}},"1":{"name":"(anonymous_6)","decl":{"start":{"line":37,"column":4},"end":{"line":37,"column":9}},"loc":{"start":{"line":38,"column":51},"end":{"line":63,"column":5}}},"2":{"name":"(anonymous_7)","decl":{"start":{"line":65,"column":4},"end":{"line":65,"column":9}},"loc":{"start":{"line":66,"column":47},"end":{"line":69,"column":5}}},"3":{"name":"(anonymous_8)","decl":{"start":{"line":71,"column":4},"end":{"line":71,"column":9}},"loc":{"start":{"line":71,"column":28},"end":{"line":78,"column":5}}},"4":{"name":"(anonymous_9)","decl":{"start":{"line":80,"column":4},"end":{"line":80,"column":9}},"loc":{"start":{"line":82,"column":49},"end":{"line":120,"column":5}}},"5":{"name":"(anonymous_10)","decl":{"start":{"line":122,"column":4},"end":{"line":122,"column":9}},"loc":{"start":{"line":122,"column":27},"end":{"line":125,"column":5}}},"6":{"name":"(anonymous_11)","decl":{"start":{"line":127,"column":4},"end":{"line":127,"column":9}},"loc":{"start":{"line":128,"column":23},"end":{"line":131,"column":5}}},"7":{"name":"(anonymous_12)","decl":{"start":{"line":137,"column":4},"end":{"line":137,"column":9}},"loc":{"start":{"line":137,"column":51},"end":{"line":201,"column":5}}},"8":{"name":"(anonymous_13)","decl":{"start":{"line":151,"column":16},"end":{"line":151,"column":17}},"loc":{"start":{"line":151,"column":24},"end":{"line":151,"column":51}}},"9":{"name":"(anonymous_14)","decl":{"start":{"line":159,"column":16},"end":{"line":159,"column":17}},"loc":{"start":{"line":159,"column":24},"end":{"line":159,"column":45}}},"10":{"name":"(anonymous_15)","decl":{"start":{"line":164,"column":24},"end":{"line":164,"column":25}},"loc":{"start":{"line":164,"column":34},"end":{"line":164,"column":43}}},"11":{"name":"(anonymous_16)","decl":{"start":{"line":170,"column":24},"end":{"line":170,"column":25}},"loc":{"start":{"line":170,"column":31},"end":{"line":170,"column":70}}},"12":{"name":"(anonymous_17)","decl":{"start":{"line":171,"column":21},"end":{"line":171,"column":22}},"loc":{"start":{"line":171,"column":28},"end":{"line":171,"column":32}}},"13":{"name":"(anonymous_18)","decl":{"start":{"line":174,"column":28},"end":{"line":174,"column":29}},"loc":{"start":{"line":174,"column":36},"end":{"line":174,"column":79}}},"14":{"name":"(anonymous_19)","decl":{"start":{"line":175,"column":25},"end":{"line":175,"column":26}},"loc":{"start":{"line":175,"column":33},"end":{"line":175,"column":43}}},"15":{"name":"(anonymous_20)","decl":{"start":{"line":209,"column":12},"end":{"line":209,"column":17}},"loc":{"start":{"line":212,"column":20},"end":{"line":224,"column":5}}},"16":{"name":"(anonymous_21)","decl":{"start":{"line":231,"column":12},"end":{"line":231,"column":17}},"loc":{"start":{"line":233,"column":22},"end":{"line":250,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":73,"column":8},"end":{"line":76,"column":14}},"type":"if","locations":[{"start":{"line":73,"column":8},"end":{"line":76,"column":14}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":87,"column":12},"end":{"line":89,"column":34}},"type":"cond-expr","locations":[{"start":{"line":88,"column":18},"end":{"line":88,"column":45}},{"start":{"line":89,"column":18},"end":{"line":89,"column":34}}]},"2":{"loc":{"start":{"line":91,"column":12},"end":{"line":93,"column":34}},"type":"cond-expr","locations":[{"start":{"line":92,"column":18},"end":{"line":92,"column":45}},{"start":{"line":93,"column":18},"end":{"line":93,"column":34}}]},"3":{"loc":{"start":{"line":95,"column":12},"end":{"line":97,"column":38}},"type":"cond-expr","locations":[{"start":{"line":96,"column":18},"end":{"line":96,"column":49}},{"start":{"line":97,"column":18},"end":{"line":97,"column":38}}]},"4":{"loc":{"start":{"line":102,"column":16},"end":{"line":104,"column":20}},"type":"cond-expr","locations":[{"start":{"line":103,"column":18},"end":{"line":103,"column":58}},{"start":{"line":104,"column":18},"end":{"line":104,"column":20}}]},"5":{"loc":{"start":{"line":105,"column":16},"end":{"line":107,"column":20}},"type":"cond-expr","locations":[{"start":{"line":106,"column":18},"end":{"line":106,"column":60}},{"start":{"line":107,"column":18},"end":{"line":107,"column":20}}]},"6":{"loc":{"start":{"line":108,"column":16},"end":{"line":110,"column":20}},"type":"cond-expr","locations":[{"start":{"line":109,"column":18},"end":{"line":109,"column":46}},{"start":{"line":110,"column":18},"end":{"line":110,"column":20}}]},"7":{"loc":{"start":{"line":111,"column":16},"end":{"line":113,"column":20}},"type":"cond-expr","locations":[{"start":{"line":112,"column":18},"end":{"line":112,"column":46}},{"start":{"line":113,"column":18},"end":{"line":113,"column":20}}]},"8":{"loc":{"start":{"line":114,"column":16},"end":{"line":116,"column":20}},"type":"cond-expr","locations":[{"start":{"line":115,"column":18},"end":{"line":115,"column":54}},{"start":{"line":116,"column":18},"end":{"line":116,"column":20}}]},"9":{"loc":{"start":{"line":146,"column":12},"end":{"line":148,"column":13}},"type":"if","locations":[{"start":{"line":146,"column":12},"end":{"line":148,"column":13}},{"start":{},"end":{}}]},"10":{"loc":{"start":{"line":154,"column":12},"end":{"line":156,"column":13}},"type":"if","locations":[{"start":{"line":154,"column":12},"end":{"line":156,"column":13}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},"type":"if","locations":[{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":219,"column":8},"end":{"line":223,"column":9}},"type":"if","locations":[{"start":{"line":219,"column":8},"end":{"line":223,"column":9}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":219,"column":12},"end":{"line":219,"column":38}},"type":"binary-expr","locations":[{"start":{"line":219,"column":12},"end":{"line":219,"column":23}},{"start":{"line":219,"column":27},"end":{"line":219,"column":38}}]},"14":{"loc":{"start":{"line":235,"column":8},"end":{"line":239,"column":9}},"type":"if","locations":[{"start":{"line":235,"column":8},"end":{"line":239,"column":9}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":240,"column":8},"end":{"line":244,"column":9}},"type":"if","locations":[{"start":{"line":240,"column":8},"end":{"line":244,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":245,"column":8},"end":{"line":249,"column":9}},"type":"if","locations":[{"start":{"line":245,"column":8},"end":{"line":249,"column":9}},{"start":{},"end":{}}]},"17":{"loc":{"start":{"line":30,"column":36},"end":{"line":30,"column":null}},"type":"cond-expr","locations":[{"start":{"line":30,"column":71},"end":{"line":30,"column":null}},{"start":{"line":30,"column":71},"end":{"line":30,"column":null}}]},"18":{"loc":{"start":{"line":30,"column":36},"end":{"line":30,"column":71}},"type":"binary-expr","locations":[{"start":{"line":30,"column":36},"end":{"line":30,"column":null}},{"start":{"line":30,"column":36},"end":{"line":30,"column":71}}]},"19":{"loc":{"start":{"line":30,"column":71},"end":{"line":32,"column":null}},"type":"cond-expr","locations":[{"start":{"line":32,"column":66},"end":{"line":32,"column":null}},{"start":{"line":32,"column":66},"end":{"line":32,"column":null}}]},"20":{"loc":{"start":{"line":30,"column":71},"end":{"line":32,"column":66}},"type":"binary-expr","locations":[{"start":{"line":30,"column":71},"end":{"line":32,"column":null}},{"start":{"line":32,"column":40},"end":{"line":32,"column":66}}]},"21":{"loc":{"start":{"line":32,"column":66},"end":{"line":33,"column":null}},"type":"cond-expr","locations":[{"start":{"line":33,"column":45},"end":{"line":33,"column":null}},{"start":{"line":33,"column":45},"end":{"line":33,"column":null}}]},"22":{"loc":{"start":{"line":32,"column":66},"end":{"line":33,"column":45}},"type":"binary-expr","locations":[{"start":{"line":32,"column":66},"end":{"line":33,"column":null}},{"start":{"line":33,"column":33},"end":{"line":33,"column":45}}]},"23":{"loc":{"start":{"line":33,"column":45},"end":{"line":34,"column":null}},"type":"cond-expr","locations":[{"start":{"line":34,"column":49},"end":{"line":34,"column":null}},{"start":{"line":34,"column":49},"end":{"line":34,"column":null}}]},"24":{"loc":{"start":{"line":33,"column":45},"end":{"line":34,"column":49}},"type":"binary-expr","locations":[{"start":{"line":33,"column":45},"end":{"line":34,"column":null}},{"start":{"line":34,"column":35},"end":{"line":34,"column":49}}]}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":3,"6":3,"7":3,"8":3,"9":18,"10":18,"11":18,"12":18,"13":6,"14":6,"15":6,"16":6,"17":6,"18":6,"19":6,"20":3,"21":1,"22":1,"23":1,"24":6,"25":6,"26":2,"27":4,"28":2,"29":2,"30":2,"31":2,"32":2,"33":1,"34":1,"35":2,"36":1,"37":1,"38":4,"39":4,"40":4,"41":4,"42":5,"43":0,"44":5,"45":10,"46":5,"47":1,"48":4,"49":7,"50":4,"51":4,"52":4,"53":7,"54":4,"55":4,"56":6,"57":6,"58":4,"59":10,"60":10,"61":4,"62":4,"63":4,"64":4,"65":3,"66":3,"67":3,"68":8,"69":2,"70":6,"71":1,"72":10,"73":2,"74":8,"75":1,"76":7,"77":1,"78":3,"79":3},"f":{"0":18,"1":6,"2":1,"3":6,"4":2,"5":2,"6":1,"7":4,"8":10,"9":7,"10":7,"11":6,"12":6,"13":10,"14":10,"15":8,"16":10},"b":{"0":[2,4],"1":[1,1],"2":[0,2],"3":[0,2],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,5],"10":[1,4],"11":[2,6],"12":[1,5],"13":[6,6],"14":[2,8],"15":[1,7],"16":[1,6],"17":[0,3],"18":[3,0],"19":[0,3],"20":[3,0],"21":[3,0],"22":[3,3],"23":[3,0],"24":[3,3]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/cycle.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/cycle.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":10,"column":0},"end":{"line":10,"column":null}},"2":{"start":{"line":16,"column":0},"end":{"line":16,"column":54}},"3":{"start":{"line":17,"column":0},"end":{"line":17,"column":null}},"4":{"start":{"line":21,"column":0},"end":{"line":21,"column":60}},"5":{"start":{"line":22,"column":0},"end":{"line":22,"column":82}},"6":{"start":{"line":23,"column":0},"end":{"line":23,"column":56}},"7":{"start":{"line":24,"column":0},"end":{"line":24,"column":null}},"8":{"start":{"line":28,"column":0},"end":{"line":28,"column":77}},"9":{"start":{"line":29,"column":0},"end":{"line":29,"column":null}},"10":{"start":{"line":33,"column":0},"end":{"line":33,"column":null}},"11":{"start":{"line":37,"column":0},"end":{"line":37,"column":null}},"12":{"start":{"line":43,"column":7},"end":{"line":313,"column":null}},"13":{"start":{"line":46,"column":25},"end":{"line":46,"column":33}},"14":{"start":{"line":48,"column":25},"end":{"line":48,"column":39}},"15":{"start":{"line":50,"column":25},"end":{"line":50,"column":34}},"16":{"start":{"line":51,"column":25},"end":{"line":51,"column":39}},"17":{"start":{"line":52,"column":25},"end":{"line":52,"column":33}},"18":{"start":{"line":54,"column":25},"end":{"line":54,"column":43}},"19":{"start":{"line":58,"column":22},"end":{"line":72,"column":10}},"20":{"start":{"line":74,"column":24},"end":{"line":74,"column":55}},"21":{"start":{"line":75,"column":8},"end":{"line":75,"column":41}},"22":{"start":{"line":79,"column":8},"end":{"line":79,"column":41}},"23":{"start":{"line":83,"column":22},"end":{"line":83,"column":52}},"24":{"start":{"line":84,"column":8},"end":{"line":85,"column":78}},"25":{"start":{"line":85,"column":12},"end":{"line":85,"column":78}},"26":{"start":{"line":86,"column":8},"end":{"line":86,"column":21}},"27":{"start":{"line":90,"column":22},"end":{"line":90,"column":44}},"28":{"start":{"line":92,"column":44},"end":{"line":114,"column":10}},"29":{"start":{"line":117,"column":32},"end":{"line":117,"column":77}},"30":{"start":{"line":119,"column":8},"end":{"line":126,"column":9}},"31":{"start":{"line":120,"column":12},"end":{"line":125,"column":14}},"32":{"start":{"line":128,"column":24},"end":{"line":128,"column":65}},"33":{"start":{"line":130,"column":8},"end":{"line":177,"column":9}},"34":{"start":{"line":132,"column":28},"end":{"line":132,"column":64}},"35":{"start":{"line":133,"column":38},"end":{"line":138,"column":null}},"36":{"start":{"line":134,"column":16},"end":{"line":138,"column":35}},"37":{"start":{"line":140,"column":41},"end":{"line":140,"column":69}},"38":{"start":{"line":142,"column":12},"end":{"line":161,"column":13}},"39":{"start":{"line":153,"column":16},"end":{"line":160,"column":18}},"40":{"start":{"line":164,"column":12},"end":{"line":166,"column":13}},"41":{"start":{"line":165,"column":16},"end":{"line":165,"column":45}},"42":{"start":{"line":168,"column":12},"end":{"line":176,"column":13}},"43":{"start":{"line":169,"column":16},"end":{"line":175,"column":18}},"44":{"start":{"line":179,"column":8},"end":{"line":179,"column":23}},"45":{"start":{"line":183,"column":22},"end":{"line":183,"column":44}},"46":{"start":{"line":185,"column":8},"end":{"line":185,"column":41}},"47":{"start":{"line":203,"column":22},"end":{"line":203,"column":49}},"48":{"start":{"line":204,"column":29},"end":{"line":204,"column":40}},"49":{"start":{"line":207,"column":35},"end":{"line":207,"column":72}},"50":{"start":{"line":209,"column":8},"end":{"line":213,"column":9}},"51":{"start":{"line":210,"column":12},"end":{"line":212,"column":14}},"52":{"start":{"line":216,"column":8},"end":{"line":234,"column":11}},"53":{"start":{"line":218,"column":12},"end":{"line":221,"column":15}},"54":{"start":{"line":224,"column":28},"end":{"line":231,"column":14}},"55":{"start":{"line":233,"column":12},"end":{"line":233,"column":52}},"56":{"start":{"line":237,"column":8},"end":{"line":247,"column":10}},"57":{"start":{"line":254,"column":8},"end":{"line":254,"column":56}},"58":{"start":{"line":263,"column":22},"end":{"line":263,"column":49}},"59":{"start":{"line":266,"column":24},"end":{"line":266,"column":65}},"60":{"start":{"line":269,"column":34},"end":{"line":272,"column":null}},"61":{"start":{"line":271,"column":16},"end":{"line":272,"column":51}},"62":{"start":{"line":274,"column":37},"end":{"line":274,"column":65}},"63":{"start":{"line":277,"column":8},"end":{"line":292,"column":9}},"64":{"start":{"line":278,"column":12},"end":{"line":284,"column":14}},"65":{"start":{"line":285,"column":15},"end":{"line":292,"column":9}},"66":{"start":{"line":286,"column":12},"end":{"line":291,"column":14}},"67":{"start":{"line":304,"column":8},"end":{"line":304,"column":36}},"68":{"start":{"line":305,"column":8},"end":{"line":311,"column":10}},"69":{"start":{"line":43,"column":13},"end":{"line":43,"column":25}},"70":{"start":{"line":43,"column":13},"end":{"line":313,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":44,"column":4},"end":{"line":44,"column":null}},"loc":{"start":{"line":54,"column":72},"end":{"line":55,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":57,"column":4},"end":{"line":57,"column":9}},"loc":{"start":{"line":57,"column":44},"end":{"line":76,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":78,"column":4},"end":{"line":78,"column":9}},"loc":{"start":{"line":78,"column":40},"end":{"line":80,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":82,"column":4},"end":{"line":82,"column":9}},"loc":{"start":{"line":82,"column":28},"end":{"line":87,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":89,"column":4},"end":{"line":89,"column":9}},"loc":{"start":{"line":89,"column":54},"end":{"line":180,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":133,"column":53},"end":{"line":133,"column":54}},"loc":{"start":{"line":134,"column":16},"end":{"line":138,"column":35}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":182,"column":4},"end":{"line":182,"column":9}},"loc":{"start":{"line":182,"column":27},"end":{"line":186,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":196,"column":4},"end":{"line":196,"column":9}},"loc":{"start":{"line":201,"column":23},"end":{"line":248,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":216,"column":39},"end":{"line":216,"column":44}},"loc":{"start":{"line":216,"column":52},"end":{"line":234,"column":9}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":253,"column":4},"end":{"line":253,"column":9}},"loc":{"start":{"line":253,"column":41},"end":{"line":255,"column":5}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":262,"column":4},"end":{"line":262,"column":9}},"loc":{"start":{"line":262,"column":39},"end":{"line":293,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":270,"column":12},"end":{"line":270,"column":13}},"loc":{"start":{"line":271,"column":16},"end":{"line":272,"column":51}}},"12":{"name":"(anonymous_16)","decl":{"start":{"line":299,"column":4},"end":{"line":299,"column":9}},"loc":{"start":{"line":302,"column":52},"end":{"line":312,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":62,"column":19},"end":{"line":62,"column":50}},"type":"binary-expr","locations":[{"start":{"line":62,"column":19},"end":{"line":62,"column":32}},{"start":{"line":62,"column":36},"end":{"line":62,"column":50}}]},"1":{"loc":{"start":{"line":64,"column":16},"end":{"line":65,"column":57}},"type":"binary-expr","locations":[{"start":{"line":64,"column":16},"end":{"line":64,"column":47}},{"start":{"line":65,"column":16},"end":{"line":65,"column":57}}]},"2":{"loc":{"start":{"line":66,"column":22},"end":{"line":66,"column":46}},"type":"binary-expr","locations":[{"start":{"line":66,"column":22},"end":{"line":66,"column":38}},{"start":{"line":66,"column":42},"end":{"line":66,"column":46}}]},"3":{"loc":{"start":{"line":84,"column":8},"end":{"line":85,"column":78}},"type":"if","locations":[{"start":{"line":84,"column":8},"end":{"line":85,"column":78}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":93,"column":16},"end":{"line":93,"column":71}},"type":"cond-expr","locations":[{"start":{"line":93,"column":44},"end":{"line":93,"column":66}},{"start":{"line":93,"column":69},"end":{"line":93,"column":71}}]},"5":{"loc":{"start":{"line":94,"column":16},"end":{"line":96,"column":20}},"type":"cond-expr","locations":[{"start":{"line":95,"column":18},"end":{"line":95,"column":52}},{"start":{"line":96,"column":18},"end":{"line":96,"column":20}}]},"6":{"loc":{"start":{"line":97,"column":16},"end":{"line":97,"column":68}},"type":"cond-expr","locations":[{"start":{"line":97,"column":43},"end":{"line":97,"column":63}},{"start":{"line":97,"column":66},"end":{"line":97,"column":68}}]},"7":{"loc":{"start":{"line":98,"column":16},"end":{"line":100,"column":20}},"type":"cond-expr","locations":[{"start":{"line":99,"column":18},"end":{"line":99,"column":46}},{"start":{"line":100,"column":18},"end":{"line":100,"column":20}}]},"8":{"loc":{"start":{"line":101,"column":16},"end":{"line":103,"column":20}},"type":"cond-expr","locations":[{"start":{"line":102,"column":18},"end":{"line":102,"column":48}},{"start":{"line":103,"column":18},"end":{"line":103,"column":20}}]},"9":{"loc":{"start":{"line":104,"column":16},"end":{"line":106,"column":20}},"type":"cond-expr","locations":[{"start":{"line":105,"column":18},"end":{"line":105,"column":58}},{"start":{"line":106,"column":18},"end":{"line":106,"column":20}}]},"10":{"loc":{"start":{"line":107,"column":16},"end":{"line":109,"column":20}},"type":"cond-expr","locations":[{"start":{"line":108,"column":18},"end":{"line":108,"column":62}},{"start":{"line":109,"column":18},"end":{"line":109,"column":20}}]},"11":{"loc":{"start":{"line":110,"column":16},"end":{"line":112,"column":20}},"type":"cond-expr","locations":[{"start":{"line":111,"column":18},"end":{"line":111,"column":62}},{"start":{"line":112,"column":18},"end":{"line":112,"column":20}}]},"12":{"loc":{"start":{"line":113,"column":16},"end":{"line":113,"column":77}},"type":"cond-expr","locations":[{"start":{"line":113,"column":46},"end":{"line":113,"column":72}},{"start":{"line":113,"column":75},"end":{"line":113,"column":77}}]},"13":{"loc":{"start":{"line":119,"column":8},"end":{"line":126,"column":9}},"type":"if","locations":[{"start":{"line":119,"column":8},"end":{"line":126,"column":9}},{"start":{},"end":{}}]},"14":{"loc":{"start":{"line":119,"column":12},"end":{"line":119,"column":53}},"type":"binary-expr","locations":[{"start":{"line":119,"column":12},"end":{"line":119,"column":36}},{"start":{"line":119,"column":40},"end":{"line":119,"column":53}}]},"15":{"loc":{"start":{"line":130,"column":8},"end":{"line":177,"column":9}},"type":"if","locations":[{"start":{"line":130,"column":8},"end":{"line":177,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":130,"column":12},"end":{"line":130,"column":70}},"type":"binary-expr","locations":[{"start":{"line":130,"column":12},"end":{"line":130,"column":23}},{"start":{"line":130,"column":28},"end":{"line":130,"column":70}}]},"17":{"loc":{"start":{"line":142,"column":12},"end":{"line":161,"column":13}},"type":"if","locations":[{"start":{"line":142,"column":12},"end":{"line":161,"column":13}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":143,"column":16},"end":{"line":151,"column":39}},"type":"binary-expr","locations":[{"start":{"line":143,"column":16},"end":{"line":143,"column":36}},{"start":{"line":144,"column":16},"end":{"line":144,"column":27}},{"start":{"line":145,"column":16},"end":{"line":151,"column":39}}]},"19":{"loc":{"start":{"line":164,"column":12},"end":{"line":166,"column":13}},"type":"if","locations":[{"start":{"line":164,"column":12},"end":{"line":166,"column":13}},{"start":{},"end":{}}]},"20":{"loc":{"start":{"line":164,"column":16},"end":{"line":164,"column":66}},"type":"binary-expr","locations":[{"start":{"line":164,"column":16},"end":{"line":164,"column":27}},{"start":{"line":164,"column":31},"end":{"line":164,"column":66}}]},"21":{"loc":{"start":{"line":168,"column":12},"end":{"line":176,"column":13}},"type":"if","locations":[{"start":{"line":168,"column":12},"end":{"line":176,"column":13}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":168,"column":16},"end":{"line":168,"column":67}},"type":"binary-expr","locations":[{"start":{"line":168,"column":16},"end":{"line":168,"column":27}},{"start":{"line":168,"column":31},"end":{"line":168,"column":67}}]},"23":{"loc":{"start":{"line":199,"column":8},"end":{"line":199,"column":41}},"type":"default-arg","locations":[{"start":{"line":199,"column":26},"end":{"line":199,"column":41}}]},"24":{"loc":{"start":{"line":200,"column":8},"end":{"line":200,"column":52}},"type":"default-arg","locations":[{"start":{"line":200,"column":35},"end":{"line":200,"column":52}}]},"25":{"loc":{"start":{"line":209,"column":8},"end":{"line":213,"column":9}},"type":"if","locations":[{"start":{"line":209,"column":8},"end":{"line":213,"column":9}},{"start":{},"end":{}}]},"26":{"loc":{"start":{"line":228,"column":29},"end":{"line":228,"column":44}},"type":"binary-expr","locations":[{"start":{"line":228,"column":29},"end":{"line":228,"column":36}},{"start":{"line":228,"column":40},"end":{"line":228,"column":44}}]},"27":{"loc":{"start":{"line":271,"column":16},"end":{"line":272,"column":51}},"type":"binary-expr","locations":[{"start":{"line":271,"column":16},"end":{"line":271,"column":55}},{"start":{"line":272,"column":16},"end":{"line":272,"column":51}}]},"28":{"loc":{"start":{"line":277,"column":8},"end":{"line":292,"column":9}},"type":"if","locations":[{"start":{"line":277,"column":8},"end":{"line":292,"column":9}},{"start":{"line":285,"column":15},"end":{"line":292,"column":9}}]},"29":{"loc":{"start":{"line":277,"column":12},"end":{"line":277,"column":55}},"type":"binary-expr","locations":[{"start":{"line":277,"column":12},"end":{"line":277,"column":33}},{"start":{"line":277,"column":37},"end":{"line":277,"column":55}}]},"30":{"loc":{"start":{"line":285,"column":15},"end":{"line":292,"column":9}},"type":"if","locations":[{"start":{"line":285,"column":15},"end":{"line":292,"column":9}},{"start":{},"end":{}}]},"31":{"loc":{"start":{"line":285,"column":19},"end":{"line":285,"column":61}},"type":"binary-expr","locations":[{"start":{"line":285,"column":19},"end":{"line":285,"column":39}},{"start":{"line":285,"column":43},"end":{"line":285,"column":61}}]},"32":{"loc":{"start":{"line":301,"column":8},"end":{"line":301,"column":41}},"type":"default-arg","locations":[{"start":{"line":301,"column":26},"end":{"line":301,"column":41}}]},"33":{"loc":{"start":{"line":302,"column":8},"end":{"line":302,"column":52}},"type":"default-arg","locations":[{"start":{"line":302,"column":35},"end":{"line":302,"column":52}}]},"34":{"loc":{"start":{"line":46,"column":33},"end":{"line":46,"column":null}},"type":"cond-expr","locations":[{"start":{"line":46,"column":52},"end":{"line":46,"column":null}},{"start":{"line":46,"column":52},"end":{"line":46,"column":null}}]},"35":{"loc":{"start":{"line":46,"column":33},"end":{"line":46,"column":52}},"type":"binary-expr","locations":[{"start":{"line":46,"column":33},"end":{"line":46,"column":null}},{"start":{"line":46,"column":33},"end":{"line":46,"column":52}}]},"36":{"loc":{"start":{"line":46,"column":52},"end":{"line":48,"column":null}},"type":"cond-expr","locations":[{"start":{"line":48,"column":70},"end":{"line":48,"column":null}},{"start":{"line":48,"column":70},"end":{"line":48,"column":null}}]},"37":{"loc":{"start":{"line":46,"column":52},"end":{"line":48,"column":70}},"type":"binary-expr","locations":[{"start":{"line":46,"column":52},"end":{"line":48,"column":null}},{"start":{"line":48,"column":39},"end":{"line":48,"column":70}}]},"38":{"loc":{"start":{"line":48,"column":70},"end":{"line":50,"column":null}},"type":"cond-expr","locations":[{"start":{"line":50,"column":54},"end":{"line":50,"column":null}},{"start":{"line":50,"column":54},"end":{"line":50,"column":null}}]},"39":{"loc":{"start":{"line":48,"column":70},"end":{"line":50,"column":54}},"type":"binary-expr","locations":[{"start":{"line":48,"column":70},"end":{"line":50,"column":null}},{"start":{"line":50,"column":34},"end":{"line":50,"column":54}}]},"40":{"loc":{"start":{"line":50,"column":54},"end":{"line":51,"column":null}},"type":"cond-expr","locations":[{"start":{"line":51,"column":52},"end":{"line":51,"column":null}},{"start":{"line":51,"column":52},"end":{"line":51,"column":null}}]},"41":{"loc":{"start":{"line":50,"column":54},"end":{"line":51,"column":52}},"type":"binary-expr","locations":[{"start":{"line":50,"column":54},"end":{"line":51,"column":null}},{"start":{"line":51,"column":39},"end":{"line":51,"column":52}}]},"42":{"loc":{"start":{"line":51,"column":52},"end":{"line":52,"column":null}},"type":"cond-expr","locations":[{"start":{"line":52,"column":46},"end":{"line":52,"column":null}},{"start":{"line":52,"column":46},"end":{"line":52,"column":null}}]},"43":{"loc":{"start":{"line":51,"column":52},"end":{"line":52,"column":46}},"type":"binary-expr","locations":[{"start":{"line":51,"column":52},"end":{"line":52,"column":null}},{"start":{"line":52,"column":33},"end":{"line":52,"column":46}}]},"44":{"loc":{"start":{"line":52,"column":46},"end":{"line":54,"column":null}},"type":"cond-expr","locations":[{"start":{"line":54,"column":72},"end":{"line":54,"column":null}},{"start":{"line":54,"column":72},"end":{"line":54,"column":null}}]},"45":{"loc":{"start":{"line":52,"column":46},"end":{"line":54,"column":72}},"type":"binary-expr","locations":[{"start":{"line":52,"column":46},"end":{"line":54,"column":null}},{"start":{"line":54,"column":43},"end":{"line":54,"column":72}}]}},"s":{"0":18,"1":18,"2":18,"3":18,"4":18,"5":18,"6":18,"7":18,"8":18,"9":18,"10":18,"11":18,"12":18,"13":21,"14":21,"15":21,"16":21,"17":21,"18":21,"19":2,"20":2,"21":2,"22":1,"23":24,"24":24,"25":2,"26":22,"27":6,"28":6,"29":6,"30":6,"31":1,"32":5,"33":5,"34":3,"35":3,"36":2,"37":3,"38":3,"39":1,"40":2,"41":1,"42":2,"43":1,"44":4,"45":2,"46":1,"47":7,"48":7,"49":7,"50":7,"51":1,"52":6,"53":6,"54":6,"55":6,"56":6,"57":1,"58":4,"59":4,"60":4,"61":4,"62":4,"63":4,"64":2,"65":2,"66":1,"67":1,"68":1,"69":18,"70":18},"f":{"0":21,"1":2,"2":1,"3":24,"4":6,"5":2,"6":2,"7":7,"8":6,"9":1,"10":4,"11":4,"12":1},"b":{"0":[2,1],"1":[2,1],"2":[2,1],"3":[2,22],"4":[1,5],"5":[1,5],"6":[0,6],"7":[0,6],"8":[0,6],"9":[0,6],"10":[0,6],"11":[0,6],"12":[0,6],"13":[1,5],"14":[6,1],"15":[3,2],"16":[5,4],"17":[1,2],"18":[3,1,1],"19":[1,1],"20":[2,2],"21":[1,1],"22":[2,2],"23":[2],"24":[2],"25":[1,6],"26":[6,4],"27":[4,4],"28":[2,2],"29":[4,3],"30":[1,1],"31":[2,1],"32":[0],"33":[0],"34":[0,18],"35":[18,0],"36":[0,18],"37":[18,0],"38":[0,18],"39":[18,0],"40":[18,0],"41":[18,18],"42":[18,0],"43":[18,18],"44":[0,18],"45":[18,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/review-scheduler.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/review-scheduler.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":52}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":56}},"3":{"start":{"line":9,"column":0},"end":{"line":9,"column":70}},"4":{"start":{"line":10,"column":0},"end":{"line":10,"column":75}},"5":{"start":{"line":11,"column":0},"end":{"line":11,"column":47}},"6":{"start":{"line":12,"column":0},"end":{"line":12,"column":49}},"7":{"start":{"line":18,"column":20},"end":{"line":24,"column":2}},"8":{"start":{"line":35,"column":35},"end":{"line":152,"column":null}},"9":{"start":{"line":36,"column":30},"end":{"line":36,"column":69}},"10":{"start":{"line":39,"column":25},"end":{"line":39,"column":40}},"11":{"start":{"line":40,"column":25},"end":{"line":40,"column":39}},"12":{"start":{"line":49,"column":8},"end":{"line":49,"column":65}},"13":{"start":{"line":51,"column":8},"end":{"line":105,"column":9}},"14":{"start":{"line":53,"column":27},"end":{"line":55,"column":14}},"15":{"start":{"line":57,"column":12},"end":{"line":100,"column":13}},"16":{"start":{"line":58,"column":16},"end":{"line":60,"column":17}},"17":{"start":{"line":59,"column":20},"end":{"line":59,"column":29}},"18":{"start":{"line":62,"column":28},"end":{"line":62,"column":38}},"19":{"start":{"line":63,"column":33},"end":{"line":63,"column":65}},"20":{"start":{"line":66,"column":16},"end":{"line":99,"column":17}},"21":{"start":{"line":68,"column":36},"end":{"line":73,"column":null}},"22":{"start":{"line":76,"column":20},"end":{"line":78,"column":22}},"23":{"start":{"line":81,"column":20},"end":{"line":98,"column":21}},"24":{"start":{"line":82,"column":24},"end":{"line":97,"column":25}},"25":{"start":{"line":83,"column":28},"end":{"line":89,"column":30}},"26":{"start":{"line":90,"column":28},"end":{"line":92,"column":30}},"27":{"start":{"line":94,"column":28},"end":{"line":96,"column":30}},"28":{"start":{"line":102,"column":12},"end":{"line":104,"column":14}},"29":{"start":{"line":114,"column":8},"end":{"line":114,"column":64}},"30":{"start":{"line":116,"column":8},"end":{"line":150,"column":9}},"31":{"start":{"line":117,"column":27},"end":{"line":120,"column":14}},"32":{"start":{"line":122,"column":24},"end":{"line":122,"column":34}},"33":{"start":{"line":124,"column":12},"end":{"line":145,"column":13}},"34":{"start":{"line":126,"column":20},"end":{"line":126,"column":66}},"35":{"start":{"line":128,"column":20},"end":{"line":129,"column":58}},"36":{"start":{"line":131,"column":16},"end":{"line":133,"column":17}},"37":{"start":{"line":132,"column":20},"end":{"line":132,"column":29}},"38":{"start":{"line":135,"column":16},"end":{"line":144,"column":17}},"39":{"start":{"line":136,"column":20},"end":{"line":136,"column":67}},"40":{"start":{"line":137,"column":20},"end":{"line":139,"column":22}},"41":{"start":{"line":141,"column":20},"end":{"line":143,"column":22}},"42":{"start":{"line":147,"column":12},"end":{"line":149,"column":14}},"43":{"start":{"line":35,"column":13},"end":{"line":35,"column":35}},"44":{"start":{"line":48,"column":10},"end":{"line":106,"column":null}},"45":{"start":{"line":113,"column":10},"end":{"line":151,"column":null}},"46":{"start":{"line":35,"column":13},"end":{"line":152,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":38,"column":4},"end":{"line":38,"column":null}},"loc":{"start":{"line":40,"column":51},"end":{"line":41,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":48,"column":10},"end":{"line":48,"column":30}},"loc":{"start":{"line":48,"column":30},"end":{"line":106,"column":5}}},"2":{"name":"(anonymous_4)","decl":{"start":{"line":113,"column":10},"end":{"line":113,"column":29}},"loc":{"start":{"line":113,"column":29},"end":{"line":151,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":58,"column":16},"end":{"line":60,"column":17}},"type":"if","locations":[{"start":{"line":58,"column":16},"end":{"line":60,"column":17}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":66,"column":16},"end":{"line":99,"column":17}},"type":"if","locations":[{"start":{"line":66,"column":16},"end":{"line":99,"column":17}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":126,"column":20},"end":{"line":126,"column":66}},"type":"binary-expr","locations":[{"start":{"line":126,"column":20},"end":{"line":126,"column":33}},{"start":{"line":126,"column":37},"end":{"line":126,"column":66}}]},"3":{"loc":{"start":{"line":128,"column":20},"end":{"line":129,"column":58}},"type":"binary-expr","locations":[{"start":{"line":128,"column":20},"end":{"line":128,"column":42}},{"start":{"line":129,"column":20},"end":{"line":129,"column":58}}]},"4":{"loc":{"start":{"line":131,"column":16},"end":{"line":133,"column":17}},"type":"if","locations":[{"start":{"line":131,"column":16},"end":{"line":133,"column":17}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":131,"column":20},"end":{"line":131,"column":61}},"type":"binary-expr","locations":[{"start":{"line":131,"column":20},"end":{"line":131,"column":34}},{"start":{"line":131,"column":38},"end":{"line":131,"column":61}}]},"6":{"loc":{"start":{"line":48,"column":34},"end":{"line":48,"column":null}},"type":"cond-expr","locations":[{"start":{"line":48,"column":41},"end":{"line":48,"column":null}},{"start":{"line":48,"column":41},"end":{"line":48,"column":null}}]},"7":{"loc":{"start":{"line":48,"column":34},"end":{"line":48,"column":41}},"type":"binary-expr","locations":[{"start":{"line":48,"column":34},"end":{"line":48,"column":null}},{"start":{"line":48,"column":34},"end":{"line":48,"column":41}}]},"8":{"loc":{"start":{"line":113,"column":33},"end":{"line":113,"column":null}},"type":"cond-expr","locations":[{"start":{"line":113,"column":40},"end":{"line":113,"column":null}},{"start":{"line":113,"column":40},"end":{"line":113,"column":null}}]},"9":{"loc":{"start":{"line":113,"column":33},"end":{"line":113,"column":40}},"type":"binary-expr","locations":[{"start":{"line":113,"column":33},"end":{"line":113,"column":null}},{"start":{"line":113,"column":33},"end":{"line":113,"column":40}}]},"10":{"loc":{"start":{"line":39,"column":40},"end":{"line":39,"column":null}},"type":"cond-expr","locations":[{"start":{"line":39,"column":53},"end":{"line":39,"column":null}},{"start":{"line":39,"column":53},"end":{"line":39,"column":null}}]},"11":{"loc":{"start":{"line":39,"column":40},"end":{"line":39,"column":53}},"type":"binary-expr","locations":[{"start":{"line":39,"column":40},"end":{"line":39,"column":null}},{"start":{"line":39,"column":40},"end":{"line":39,"column":53}}]},"12":{"loc":{"start":{"line":39,"column":53},"end":{"line":40,"column":null}},"type":"cond-expr","locations":[{"start":{"line":40,"column":51},"end":{"line":40,"column":null}},{"start":{"line":40,"column":51},"end":{"line":40,"column":null}}]},"13":{"loc":{"start":{"line":39,"column":53},"end":{"line":40,"column":51}},"type":"binary-expr","locations":[{"start":{"line":39,"column":53},"end":{"line":40,"column":null}},{"start":{"line":40,"column":39},"end":{"line":40,"column":51}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":10,"10":10,"11":10,"12":5,"13":5,"14":5,"15":4,"16":4,"17":1,"18":3,"19":3,"20":3,"21":2,"22":2,"23":2,"24":4,"25":4,"26":3,"27":1,"28":1,"29":5,"30":5,"31":5,"32":4,"33":4,"34":5,"35":5,"36":5,"37":1,"38":4,"39":4,"40":3,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1},"f":{"0":10,"1":5,"2":5},"b":{"0":[1,3],"1":[2,1],"2":[5,5],"3":[5,2],"4":[1,4],"5":[5,2],"6":[1,0],"7":[1,1],"8":[1,0],"9":[1,1],"10":[1,0],"11":[1,1],"12":[1,0],"13":[1,1]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/review.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/feedback360/application/services/review.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":24,"column":0},"end":{"line":24,"column":null}},"2":{"start":{"line":31,"column":0},"end":{"line":31,"column":54}},"3":{"start":{"line":33,"column":0},"end":{"line":33,"column":97}},"4":{"start":{"line":34,"column":0},"end":{"line":34,"column":110}},"5":{"start":{"line":35,"column":0},"end":{"line":35,"column":60}},"6":{"start":{"line":36,"column":0},"end":{"line":36,"column":58}},"7":{"start":{"line":38,"column":0},"end":{"line":38,"column":71}},"8":{"start":{"line":39,"column":0},"end":{"line":39,"column":62}},"9":{"start":{"line":40,"column":0},"end":{"line":40,"column":66}},"10":{"start":{"line":41,"column":0},"end":{"line":41,"column":92}},"11":{"start":{"line":42,"column":0},"end":{"line":42,"column":84}},"12":{"start":{"line":43,"column":0},"end":{"line":43,"column":58}},"13":{"start":{"line":44,"column":0},"end":{"line":44,"column":62}},"14":{"start":{"line":45,"column":0},"end":{"line":45,"column":null}},"15":{"start":{"line":49,"column":0},"end":{"line":49,"column":null}},"16":{"start":{"line":53,"column":0},"end":{"line":53,"column":89}},"17":{"start":{"line":54,"column":0},"end":{"line":54,"column":79}},"18":{"start":{"line":55,"column":0},"end":{"line":55,"column":null}},"19":{"start":{"line":59,"column":0},"end":{"line":59,"column":null}},"20":{"start":{"line":63,"column":0},"end":{"line":63,"column":null}},"21":{"start":{"line":67,"column":0},"end":{"line":67,"column":null}},"22":{"start":{"line":71,"column":0},"end":{"line":71,"column":null}},"23":{"start":{"line":75,"column":0},"end":{"line":75,"column":null}},"24":{"start":{"line":79,"column":0},"end":{"line":79,"column":null}},"25":{"start":{"line":83,"column":0},"end":{"line":83,"column":null}},"26":{"start":{"line":87,"column":0},"end":{"line":87,"column":47}},"27":{"start":{"line":90,"column":7},"end":{"line":1025,"column":null}},"28":{"start":{"line":93,"column":25},"end":{"line":93,"column":34}},"29":{"start":{"line":95,"column":25},"end":{"line":95,"column":36}},"30":{"start":{"line":97,"column":25},"end":{"line":97,"column":44}},"31":{"start":{"line":99,"column":25},"end":{"line":99,"column":34}},"32":{"start":{"line":101,"column":25},"end":{"line":101,"column":38}},"33":{"start":{"line":103,"column":25},"end":{"line":103,"column":36}},"34":{"start":{"line":105,"column":25},"end":{"line":105,"column":40}},"35":{"start":{"line":107,"column":25},"end":{"line":107,"column":39}},"36":{"start":{"line":108,"column":25},"end":{"line":108,"column":33}},"37":{"start":{"line":109,"column":25},"end":{"line":109,"column":44}},"38":{"start":{"line":110,"column":25},"end":{"line":110,"column":38}},"39":{"start":{"line":111,"column":25},"end":{"line":111,"column":33}},"40":{"start":{"line":112,"column":25},"end":{"line":112,"column":39}},"41":{"start":{"line":116,"column":8},"end":{"line":118,"column":9}},"42":{"start":{"line":117,"column":12},"end":{"line":117,"column":55}},"43":{"start":{"line":120,"column":23},"end":{"line":137,"column":10}},"44":{"start":{"line":139,"column":24},"end":{"line":139,"column":57}},"45":{"start":{"line":140,"column":8},"end":{"line":140,"column":41}},"46":{"start":{"line":147,"column":24},"end":{"line":147,"column":56}},"47":{"start":{"line":149,"column":32},"end":{"line":159,"column":50}},"48":{"start":{"line":152,"column":46},"end":{"line":154,"column":null}},"49":{"start":{"line":156,"column":20},"end":{"line":156,"column":66}},"50":{"start":{"line":159,"column":29},"end":{"line":159,"column":49}},"51":{"start":{"line":161,"column":8},"end":{"line":161,"column":31}},"52":{"start":{"line":165,"column":23},"end":{"line":165,"column":54}},"53":{"start":{"line":166,"column":8},"end":{"line":167,"column":79}},"54":{"start":{"line":167,"column":12},"end":{"line":167,"column":79}},"55":{"start":{"line":169,"column":8},"end":{"line":169,"column":54}},"56":{"start":{"line":171,"column":8},"end":{"line":171,"column":22}},"57":{"start":{"line":184,"column":8},"end":{"line":184,"column":27}},"58":{"start":{"line":184,"column":20},"end":{"line":184,"column":27}},"59":{"start":{"line":187,"column":12},"end":{"line":188,"column":51}},"60":{"start":{"line":190,"column":8},"end":{"line":190,"column":32}},"61":{"start":{"line":190,"column":25},"end":{"line":190,"column":32}},"62":{"start":{"line":192,"column":34},"end":{"line":192,"column":63}},"63":{"start":{"line":193,"column":32},"end":{"line":193,"column":59}},"64":{"start":{"line":195,"column":8},"end":{"line":195,"column":57}},"65":{"start":{"line":195,"column":50},"end":{"line":195,"column":57}},"66":{"start":{"line":197,"column":26},"end":{"line":199,"column":10}},"67":{"start":{"line":201,"column":8},"end":{"line":201,"column":41}},"68":{"start":{"line":201,"column":34},"end":{"line":201,"column":41}},"69":{"start":{"line":203,"column":8},"end":{"line":205,"column":10}},"70":{"start":{"line":212,"column":8},"end":{"line":212,"column":33}},"71":{"start":{"line":212,"column":20},"end":{"line":212,"column":33}},"72":{"start":{"line":215,"column":12},"end":{"line":216,"column":51}},"73":{"start":{"line":218,"column":8},"end":{"line":218,"column":37}},"74":{"start":{"line":218,"column":25},"end":{"line":218,"column":37}},"75":{"start":{"line":220,"column":34},"end":{"line":220,"column":63}},"76":{"start":{"line":221,"column":32},"end":{"line":221,"column":59}},"77":{"start":{"line":223,"column":8},"end":{"line":223,"column":62}},"78":{"start":{"line":223,"column":50},"end":{"line":223,"column":62}},"79":{"start":{"line":225,"column":28},"end":{"line":227,"column":10}},"80":{"start":{"line":229,"column":8},"end":{"line":229,"column":48}},"81":{"start":{"line":229,"column":36},"end":{"line":229,"column":48}},"82":{"start":{"line":231,"column":26},"end":{"line":233,"column":10}},"83":{"start":{"line":235,"column":8},"end":{"line":235,"column":46}},"84":{"start":{"line":235,"column":34},"end":{"line":235,"column":46}},"85":{"start":{"line":237,"column":8},"end":{"line":237,"column":21}},"86":{"start":{"line":250,"column":8},"end":{"line":250,"column":27}},"87":{"start":{"line":250,"column":20},"end":{"line":250,"column":27}},"88":{"start":{"line":253,"column":12},"end":{"line":254,"column":51}},"89":{"start":{"line":256,"column":8},"end":{"line":256,"column":32}},"90":{"start":{"line":256,"column":25},"end":{"line":256,"column":32}},"91":{"start":{"line":258,"column":34},"end":{"line":258,"column":63}},"92":{"start":{"line":259,"column":32},"end":{"line":259,"column":59}},"93":{"start":{"line":261,"column":8},"end":{"line":261,"column":57}},"94":{"start":{"line":261,"column":50},"end":{"line":261,"column":57}},"95":{"start":{"line":263,"column":28},"end":{"line":265,"column":10}},"96":{"start":{"line":267,"column":8},"end":{"line":267,"column":43}},"97":{"start":{"line":267,"column":36},"end":{"line":267,"column":43}},"98":{"start":{"line":269,"column":26},"end":{"line":271,"column":10}},"99":{"start":{"line":273,"column":8},"end":{"line":273,"column":41}},"100":{"start":{"line":273,"column":34},"end":{"line":273,"column":41}},"101":{"start":{"line":275,"column":8},"end":{"line":277,"column":10}},"102":{"start":{"line":285,"column":23},"end":{"line":285,"column":45}},"103":{"start":{"line":287,"column":8},"end":{"line":297,"column":9}},"104":{"start":{"line":291,"column":12},"end":{"line":296,"column":14}},"105":{"start":{"line":299,"column":8},"end":{"line":312,"column":9}},"106":{"start":{"line":300,"column":26},"end":{"line":300,"column":66}},"107":{"start":{"line":301,"column":12},"end":{"line":311,"column":13}},"108":{"start":{"line":305,"column":16},"end":{"line":310,"column":18}},"109":{"start":{"line":314,"column":45},"end":{"line":350,"column":10}},"110":{"start":{"line":352,"column":30},"end":{"line":352,"column":72}},"111":{"start":{"line":355,"column":8},"end":{"line":361,"column":9}},"112":{"start":{"line":360,"column":12},"end":{"line":360,"column":42}},"113":{"start":{"line":363,"column":8},"end":{"line":375,"column":9}},"114":{"start":{"line":368,"column":12},"end":{"line":374,"column":14}},"115":{"start":{"line":377,"column":8},"end":{"line":377,"column":29}},"116":{"start":{"line":381,"column":8},"end":{"line":381,"column":31}},"117":{"start":{"line":383,"column":32},"end":{"line":383,"column":73}},"118":{"start":{"line":384,"column":8},"end":{"line":390,"column":9}},"119":{"start":{"line":385,"column":12},"end":{"line":389,"column":14}},"120":{"start":{"line":392,"column":8},"end":{"line":392,"column":42}},"121":{"start":{"line":398,"column":8},"end":{"line":411,"column":9}},"122":{"start":{"line":399,"column":26},"end":{"line":399,"column":68}},"123":{"start":{"line":400,"column":12},"end":{"line":410,"column":13}},"124":{"start":{"line":404,"column":16},"end":{"line":409,"column":18}},"125":{"start":{"line":413,"column":29},"end":{"line":415,"column":18}},"126":{"start":{"line":417,"column":22},"end":{"line":417,"column":58}},"127":{"start":{"line":418,"column":27},"end":{"line":418,"column":73}},"128":{"start":{"line":420,"column":8},"end":{"line":424,"column":9}},"129":{"start":{"line":421,"column":12},"end":{"line":423,"column":14}},"130":{"start":{"line":426,"column":25},"end":{"line":437,"column":10}},"131":{"start":{"line":439,"column":8},"end":{"line":439,"column":47}},"132":{"start":{"line":443,"column":8},"end":{"line":443,"column":44}},"133":{"start":{"line":447,"column":25},"end":{"line":447,"column":58}},"134":{"start":{"line":449,"column":8},"end":{"line":453,"column":9}},"135":{"start":{"line":450,"column":12},"end":{"line":452,"column":14}},"136":{"start":{"line":455,"column":8},"end":{"line":468,"column":9}},"137":{"start":{"line":456,"column":26},"end":{"line":456,"column":69}},"138":{"start":{"line":457,"column":12},"end":{"line":467,"column":13}},"139":{"start":{"line":461,"column":16},"end":{"line":466,"column":18}},"140":{"start":{"line":470,"column":8},"end":{"line":470,"column":44}},"141":{"start":{"line":476,"column":23},"end":{"line":476,"column":59}},"142":{"start":{"line":478,"column":8},"end":{"line":485,"column":9}},"143":{"start":{"line":479,"column":12},"end":{"line":484,"column":14}},"144":{"start":{"line":487,"column":33},"end":{"line":488,"column":null}},"145":{"start":{"line":490,"column":27},"end":{"line":491,"column":null}},"146":{"start":{"line":497,"column":34},"end":{"line":500,"column":10}},"147":{"start":{"line":502,"column":12},"end":{"line":512,"column":14}},"148":{"start":{"line":514,"column":25},"end":{"line":524,"column":10}},"149":{"start":{"line":526,"column":8},"end":{"line":526,"column":53}},"150":{"start":{"line":534,"column":8},"end":{"line":534,"column":44}},"151":{"start":{"line":535,"column":8},"end":{"line":535,"column":68}},"152":{"start":{"line":539,"column":23},"end":{"line":539,"column":51}},"153":{"start":{"line":541,"column":8},"end":{"line":548,"column":9}},"154":{"start":{"line":542,"column":12},"end":{"line":547,"column":14}},"155":{"start":{"line":550,"column":8},"end":{"line":550,"column":66}},"156":{"start":{"line":557,"column":23},"end":{"line":557,"column":66}},"157":{"start":{"line":559,"column":8},"end":{"line":569,"column":9}},"158":{"start":{"line":563,"column":12},"end":{"line":568,"column":14}},"159":{"start":{"line":571,"column":23},"end":{"line":578,"column":10}},"160":{"start":{"line":580,"column":8},"end":{"line":580,"column":49}},"161":{"start":{"line":588,"column":23},"end":{"line":588,"column":51}},"162":{"start":{"line":589,"column":8},"end":{"line":589,"column":61}},"163":{"start":{"line":591,"column":8},"end":{"line":591,"column":77}},"164":{"start":{"line":598,"column":23},"end":{"line":598,"column":51}},"165":{"start":{"line":599,"column":8},"end":{"line":599,"column":61}},"166":{"start":{"line":602,"column":12},"end":{"line":602,"column":78}},"167":{"start":{"line":604,"column":8},"end":{"line":604,"column":61}},"168":{"start":{"line":604,"column":41},"end":{"line":604,"column":56}},"169":{"start":{"line":610,"column":23},"end":{"line":610,"column":59}},"170":{"start":{"line":612,"column":8},"end":{"line":626,"column":9}},"171":{"start":{"line":618,"column":12},"end":{"line":625,"column":14}},"172":{"start":{"line":628,"column":25},"end":{"line":643,"column":10}},"173":{"start":{"line":645,"column":8},"end":{"line":645,"column":49}},"174":{"start":{"line":653,"column":8},"end":{"line":653,"column":72}},"175":{"start":{"line":655,"column":25},"end":{"line":655,"column":67}},"176":{"start":{"line":656,"column":23},"end":{"line":656,"column":60}},"177":{"start":{"line":658,"column":8},"end":{"line":668,"column":9}},"178":{"start":{"line":662,"column":12},"end":{"line":667,"column":14}},"179":{"start":{"line":670,"column":31},"end":{"line":670,"column":51}},"180":{"start":{"line":671,"column":8},"end":{"line":671,"column":41}},"181":{"start":{"line":673,"column":24},"end":{"line":673,"column":76}},"182":{"start":{"line":675,"column":8},"end":{"line":684,"column":9}},"183":{"start":{"line":676,"column":12},"end":{"line":683,"column":14}},"184":{"start":{"line":686,"column":8},"end":{"line":686,"column":23}},"185":{"start":{"line":705,"column":23},"end":{"line":705,"column":51}},"186":{"start":{"line":706,"column":27},"end":{"line":706,"column":69}},"187":{"start":{"line":707,"column":8},"end":{"line":711,"column":9}},"188":{"start":{"line":708,"column":12},"end":{"line":710,"column":14}},"189":{"start":{"line":712,"column":30},"end":{"line":712,"column":55}},"190":{"start":{"line":715,"column":35},"end":{"line":715,"column":77}},"191":{"start":{"line":716,"column":8},"end":{"line":720,"column":9}},"192":{"start":{"line":717,"column":12},"end":{"line":719,"column":14}},"193":{"start":{"line":722,"column":8},"end":{"line":724,"column":11}},"194":{"start":{"line":726,"column":8},"end":{"line":737,"column":10}},"195":{"start":{"line":750,"column":8},"end":{"line":750,"column":27}},"196":{"start":{"line":750,"column":20},"end":{"line":750,"column":27}},"197":{"start":{"line":752,"column":24},"end":{"line":752,"column":66}},"198":{"start":{"line":754,"column":8},"end":{"line":754,"column":28}},"199":{"start":{"line":754,"column":21},"end":{"line":754,"column":28}},"200":{"start":{"line":756,"column":27},"end":{"line":756,"column":69}},"201":{"start":{"line":758,"column":8},"end":{"line":758,"column":57}},"202":{"start":{"line":758,"column":50},"end":{"line":758,"column":57}},"203":{"start":{"line":760,"column":8},"end":{"line":764,"column":10}},"204":{"start":{"line":772,"column":23},"end":{"line":772,"column":51}},"205":{"start":{"line":774,"column":8},"end":{"line":778,"column":9}},"206":{"start":{"line":775,"column":12},"end":{"line":777,"column":14}},"207":{"start":{"line":780,"column":8},"end":{"line":780,"column":62}},"208":{"start":{"line":784,"column":8},"end":{"line":784,"column":46}},"209":{"start":{"line":788,"column":8},"end":{"line":788,"column":45}},"210":{"start":{"line":789,"column":25},"end":{"line":797,"column":10}},"211":{"start":{"line":798,"column":8},"end":{"line":798,"column":47}},"212":{"start":{"line":806,"column":8},"end":{"line":806,"column":59}},"213":{"start":{"line":807,"column":8},"end":{"line":807,"column":60}},"214":{"start":{"line":820,"column":8},"end":{"line":820,"column":27}},"215":{"start":{"line":820,"column":20},"end":{"line":820,"column":27}},"216":{"start":{"line":823,"column":12},"end":{"line":824,"column":51}},"217":{"start":{"line":826,"column":8},"end":{"line":826,"column":32}},"218":{"start":{"line":826,"column":25},"end":{"line":826,"column":32}},"219":{"start":{"line":828,"column":23},"end":{"line":828,"column":51}},"220":{"start":{"line":830,"column":34},"end":{"line":830,"column":63}},"221":{"start":{"line":831,"column":32},"end":{"line":831,"column":59}},"222":{"start":{"line":833,"column":8},"end":{"line":833,"column":57}},"223":{"start":{"line":833,"column":50},"end":{"line":833,"column":57}},"224":{"start":{"line":835,"column":28},"end":{"line":837,"column":10}},"225":{"start":{"line":839,"column":8},"end":{"line":839,"column":43}},"226":{"start":{"line":839,"column":36},"end":{"line":839,"column":43}},"227":{"start":{"line":841,"column":26},"end":{"line":843,"column":10}},"228":{"start":{"line":845,"column":8},"end":{"line":845,"column":41}},"229":{"start":{"line":845,"column":34},"end":{"line":845,"column":41}},"230":{"start":{"line":847,"column":8},"end":{"line":851,"column":10}},"231":{"start":{"line":855,"column":8},"end":{"line":855,"column":44}},"232":{"start":{"line":861,"column":8},"end":{"line":863,"column":9}},"233":{"start":{"line":862,"column":12},"end":{"line":862,"column":55}},"234":{"start":{"line":865,"column":22},"end":{"line":872,"column":10}},"235":{"start":{"line":874,"column":8},"end":{"line":874,"column":48}},"236":{"start":{"line":880,"column":8},"end":{"line":880,"column":46}},"237":{"start":{"line":886,"column":8},"end":{"line":886,"column":46}},"238":{"start":{"line":892,"column":8},"end":{"line":892,"column":56}},"239":{"start":{"line":896,"column":8},"end":{"line":896,"column":48}},"240":{"start":{"line":914,"column":23},"end":{"line":914,"column":51}},"241":{"start":{"line":915,"column":29},"end":{"line":915,"column":41}},"242":{"start":{"line":918,"column":35},"end":{"line":918,"column":73}},"243":{"start":{"line":920,"column":8},"end":{"line":924,"column":9}},"244":{"start":{"line":921,"column":12},"end":{"line":923,"column":14}},"245":{"start":{"line":927,"column":8},"end":{"line":945,"column":11}},"246":{"start":{"line":929,"column":12},"end":{"line":932,"column":15}},"247":{"start":{"line":935,"column":28},"end":{"line":942,"column":14}},"248":{"start":{"line":944,"column":12},"end":{"line":944,"column":52}},"249":{"start":{"line":948,"column":8},"end":{"line":958,"column":10}},"250":{"start":{"line":967,"column":8},"end":{"line":967,"column":58}},"251":{"start":{"line":976,"column":8},"end":{"line":976,"column":37}},"252":{"start":{"line":979,"column":28},"end":{"line":979,"column":77}},"253":{"start":{"line":982,"column":38},"end":{"line":985,"column":null}},"254":{"start":{"line":984,"column":16},"end":{"line":985,"column":63}},"255":{"start":{"line":987,"column":36},"end":{"line":987,"column":68}},"256":{"start":{"line":990,"column":8},"end":{"line":1005,"column":9}},"257":{"start":{"line":991,"column":12},"end":{"line":997,"column":14}},"258":{"start":{"line":999,"column":12},"end":{"line":1004,"column":14}},"259":{"start":{"line":1017,"column":8},"end":{"line":1023,"column":10}},"260":{"start":{"line":90,"column":13},"end":{"line":90,"column":26}},"261":{"start":{"line":90,"column":13},"end":{"line":1025,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":91,"column":4},"end":{"line":91,"column":null}},"loc":{"start":{"line":112,"column":52},"end":{"line":113,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":115,"column":4},"end":{"line":115,"column":9}},"loc":{"start":{"line":115,"column":45},"end":{"line":141,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":143,"column":4},"end":{"line":143,"column":9}},"loc":{"start":{"line":145,"column":26},"end":{"line":162,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":151,"column":28},"end":{"line":151,"column":33}},"loc":{"start":{"line":151,"column":45},"end":{"line":157,"column":17}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":159,"column":17},"end":{"line":159,"column":18}},"loc":{"start":{"line":159,"column":29},"end":{"line":159,"column":49}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":164,"column":4},"end":{"line":164,"column":9}},"loc":{"start":{"line":164,"column":48},"end":{"line":172,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":180,"column":12},"end":{"line":180,"column":17}},"loc":{"start":{"line":182,"column":26},"end":{"line":206,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":208,"column":12},"end":{"line":208,"column":17}},"loc":{"start":{"line":210,"column":26},"end":{"line":238,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":246,"column":12},"end":{"line":246,"column":17}},"loc":{"start":{"line":248,"column":26},"end":{"line":278,"column":5}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":280,"column":4},"end":{"line":280,"column":9}},"loc":{"start":{"line":283,"column":25},"end":{"line":378,"column":5}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":380,"column":4},"end":{"line":380,"column":9}},"loc":{"start":{"line":380,"column":27},"end":{"line":393,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":395,"column":4},"end":{"line":395,"column":9}},"loc":{"start":{"line":396,"column":38},"end":{"line":440,"column":5}}},"12":{"name":"(anonymous_16)","decl":{"start":{"line":442,"column":4},"end":{"line":442,"column":9}},"loc":{"start":{"line":442,"column":50},"end":{"line":444,"column":5}}},"13":{"name":"(anonymous_17)","decl":{"start":{"line":446,"column":4},"end":{"line":446,"column":9}},"loc":{"start":{"line":446,"column":35},"end":{"line":471,"column":5}}},"14":{"name":"(anonymous_18)","decl":{"start":{"line":473,"column":4},"end":{"line":473,"column":9}},"loc":{"start":{"line":474,"column":43},"end":{"line":527,"column":5}}},"15":{"name":"(anonymous_19)","decl":{"start":{"line":529,"column":4},"end":{"line":529,"column":9}},"loc":{"start":{"line":532,"column":26},"end":{"line":536,"column":5}}},"16":{"name":"(anonymous_20)","decl":{"start":{"line":538,"column":4},"end":{"line":538,"column":9}},"loc":{"start":{"line":538,"column":61},"end":{"line":551,"column":5}}},"17":{"name":"(anonymous_21)","decl":{"start":{"line":553,"column":4},"end":{"line":553,"column":9}},"loc":{"start":{"line":555,"column":25},"end":{"line":581,"column":5}}},"18":{"name":"(anonymous_22)","decl":{"start":{"line":583,"column":4},"end":{"line":583,"column":9}},"loc":{"start":{"line":586,"column":26},"end":{"line":592,"column":5}}},"19":{"name":"(anonymous_23)","decl":{"start":{"line":594,"column":4},"end":{"line":594,"column":9}},"loc":{"start":{"line":596,"column":26},"end":{"line":605,"column":5}}},"20":{"name":"(anonymous_24)","decl":{"start":{"line":604,"column":29},"end":{"line":604,"column":30}},"loc":{"start":{"line":604,"column":41},"end":{"line":604,"column":56}}},"21":{"name":"(anonymous_25)","decl":{"start":{"line":607,"column":4},"end":{"line":607,"column":9}},"loc":{"start":{"line":608,"column":37},"end":{"line":646,"column":5}}},"22":{"name":"(anonymous_26)","decl":{"start":{"line":648,"column":4},"end":{"line":648,"column":9}},"loc":{"start":{"line":651,"column":25},"end":{"line":687,"column":5}}},"23":{"name":"(anonymous_27)","decl":{"start":{"line":697,"column":4},"end":{"line":697,"column":9}},"loc":{"start":{"line":703,"column":69},"end":{"line":738,"column":5}}},"24":{"name":"(anonymous_28)","decl":{"start":{"line":746,"column":12},"end":{"line":746,"column":17}},"loc":{"start":{"line":748,"column":26},"end":{"line":765,"column":5}}},"25":{"name":"(anonymous_29)","decl":{"start":{"line":767,"column":4},"end":{"line":767,"column":9}},"loc":{"start":{"line":770,"column":26},"end":{"line":781,"column":5}}},"26":{"name":"(anonymous_30)","decl":{"start":{"line":783,"column":4},"end":{"line":783,"column":9}},"loc":{"start":{"line":783,"column":37},"end":{"line":785,"column":5}}},"27":{"name":"(anonymous_31)","decl":{"start":{"line":787,"column":4},"end":{"line":787,"column":9}},"loc":{"start":{"line":787,"column":49},"end":{"line":799,"column":5}}},"28":{"name":"(anonymous_32)","decl":{"start":{"line":801,"column":4},"end":{"line":801,"column":9}},"loc":{"start":{"line":804,"column":25},"end":{"line":808,"column":5}}},"29":{"name":"(anonymous_33)","decl":{"start":{"line":816,"column":12},"end":{"line":816,"column":17}},"loc":{"start":{"line":818,"column":26},"end":{"line":852,"column":5}}},"30":{"name":"(anonymous_34)","decl":{"start":{"line":854,"column":4},"end":{"line":854,"column":9}},"loc":{"start":{"line":854,"column":35},"end":{"line":856,"column":5}}},"31":{"name":"(anonymous_35)","decl":{"start":{"line":858,"column":4},"end":{"line":858,"column":9}},"loc":{"start":{"line":859,"column":42},"end":{"line":875,"column":5}}},"32":{"name":"(anonymous_36)","decl":{"start":{"line":877,"column":4},"end":{"line":877,"column":9}},"loc":{"start":{"line":878,"column":38},"end":{"line":881,"column":5}}},"33":{"name":"(anonymous_37)","decl":{"start":{"line":883,"column":4},"end":{"line":883,"column":9}},"loc":{"start":{"line":884,"column":18},"end":{"line":887,"column":5}}},"34":{"name":"(anonymous_38)","decl":{"start":{"line":889,"column":4},"end":{"line":889,"column":9}},"loc":{"start":{"line":890,"column":23},"end":{"line":893,"column":5}}},"35":{"name":"(anonymous_39)","decl":{"start":{"line":895,"column":4},"end":{"line":895,"column":9}},"loc":{"start":{"line":895,"column":39},"end":{"line":897,"column":5}}},"36":{"name":"(anonymous_40)","decl":{"start":{"line":907,"column":4},"end":{"line":907,"column":9}},"loc":{"start":{"line":912,"column":23},"end":{"line":959,"column":5}}},"37":{"name":"(anonymous_41)","decl":{"start":{"line":927,"column":39},"end":{"line":927,"column":44}},"loc":{"start":{"line":927,"column":52},"end":{"line":945,"column":9}}},"38":{"name":"(anonymous_42)","decl":{"start":{"line":964,"column":4},"end":{"line":964,"column":9}},"loc":{"start":{"line":965,"column":24},"end":{"line":968,"column":5}}},"39":{"name":"(anonymous_43)","decl":{"start":{"line":975,"column":4},"end":{"line":975,"column":9}},"loc":{"start":{"line":975,"column":41},"end":{"line":1006,"column":5}}},"40":{"name":"(anonymous_44)","decl":{"start":{"line":983,"column":12},"end":{"line":983,"column":13}},"loc":{"start":{"line":984,"column":16},"end":{"line":985,"column":63}}},"41":{"name":"(anonymous_45)","decl":{"start":{"line":1012,"column":4},"end":{"line":1012,"column":9}},"loc":{"start":{"line":1015,"column":25},"end":{"line":1024,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":116,"column":8},"end":{"line":118,"column":9}},"type":"if","locations":[{"start":{"line":116,"column":8},"end":{"line":118,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":128,"column":20},"end":{"line":128,"column":42}},"type":"binary-expr","locations":[{"start":{"line":128,"column":20},"end":{"line":128,"column":34}},{"start":{"line":128,"column":38},"end":{"line":128,"column":42}}]},"2":{"loc":{"start":{"line":129,"column":23},"end":{"line":129,"column":48}},"type":"binary-expr","locations":[{"start":{"line":129,"column":23},"end":{"line":129,"column":40}},{"start":{"line":129,"column":44},"end":{"line":129,"column":48}}]},"3":{"loc":{"start":{"line":130,"column":23},"end":{"line":130,"column":48}},"type":"binary-expr","locations":[{"start":{"line":130,"column":23},"end":{"line":130,"column":40}},{"start":{"line":130,"column":44},"end":{"line":130,"column":48}}]},"4":{"loc":{"start":{"line":131,"column":29},"end":{"line":131,"column":60}},"type":"binary-expr","locations":[{"start":{"line":131,"column":29},"end":{"line":131,"column":52}},{"start":{"line":131,"column":56},"end":{"line":131,"column":60}}]},"5":{"loc":{"start":{"line":132,"column":31},"end":{"line":132,"column":64}},"type":"binary-expr","locations":[{"start":{"line":132,"column":31},"end":{"line":132,"column":56}},{"start":{"line":132,"column":60},"end":{"line":132,"column":64}}]},"6":{"loc":{"start":{"line":133,"column":34},"end":{"line":133,"column":70}},"type":"binary-expr","locations":[{"start":{"line":133,"column":34},"end":{"line":133,"column":62}},{"start":{"line":133,"column":66},"end":{"line":133,"column":70}}]},"7":{"loc":{"start":{"line":134,"column":21},"end":{"line":134,"column":44}},"type":"binary-expr","locations":[{"start":{"line":134,"column":21},"end":{"line":134,"column":36}},{"start":{"line":134,"column":40},"end":{"line":134,"column":44}}]},"8":{"loc":{"start":{"line":135,"column":19},"end":{"line":135,"column":51}},"type":"binary-expr","locations":[{"start":{"line":135,"column":19},"end":{"line":135,"column":32}},{"start":{"line":135,"column":36},"end":{"line":135,"column":51}}]},"9":{"loc":{"start":{"line":136,"column":22},"end":{"line":136,"column":46}},"type":"binary-expr","locations":[{"start":{"line":136,"column":22},"end":{"line":136,"column":38}},{"start":{"line":136,"column":42},"end":{"line":136,"column":46}}]},"10":{"loc":{"start":{"line":156,"column":27},"end":{"line":156,"column":65}},"type":"cond-expr","locations":[{"start":{"line":156,"column":47},"end":{"line":156,"column":53}},{"start":{"line":156,"column":56},"end":{"line":156,"column":65}}]},"11":{"loc":{"start":{"line":166,"column":8},"end":{"line":167,"column":79}},"type":"if","locations":[{"start":{"line":166,"column":8},"end":{"line":167,"column":79}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":184,"column":8},"end":{"line":184,"column":27}},"type":"if","locations":[{"start":{"line":184,"column":8},"end":{"line":184,"column":27}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":187,"column":12},"end":{"line":188,"column":51}},"type":"binary-expr","locations":[{"start":{"line":187,"column":12},"end":{"line":187,"column":54}},{"start":{"line":188,"column":12},"end":{"line":188,"column":51}}]},"14":{"loc":{"start":{"line":190,"column":8},"end":{"line":190,"column":32}},"type":"if","locations":[{"start":{"line":190,"column":8},"end":{"line":190,"column":32}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":195,"column":8},"end":{"line":195,"column":57}},"type":"if","locations":[{"start":{"line":195,"column":8},"end":{"line":195,"column":57}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":195,"column":12},"end":{"line":195,"column":48}},"type":"binary-expr","locations":[{"start":{"line":195,"column":12},"end":{"line":195,"column":29}},{"start":{"line":195,"column":33},"end":{"line":195,"column":48}}]},"17":{"loc":{"start":{"line":201,"column":8},"end":{"line":201,"column":41}},"type":"if","locations":[{"start":{"line":201,"column":8},"end":{"line":201,"column":41}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":212,"column":8},"end":{"line":212,"column":33}},"type":"if","locations":[{"start":{"line":212,"column":8},"end":{"line":212,"column":33}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":215,"column":12},"end":{"line":216,"column":51}},"type":"binary-expr","locations":[{"start":{"line":215,"column":12},"end":{"line":215,"column":54}},{"start":{"line":216,"column":12},"end":{"line":216,"column":51}}]},"20":{"loc":{"start":{"line":218,"column":8},"end":{"line":218,"column":37}},"type":"if","locations":[{"start":{"line":218,"column":8},"end":{"line":218,"column":37}},{"start":{},"end":{}}]},"21":{"loc":{"start":{"line":223,"column":8},"end":{"line":223,"column":62}},"type":"if","locations":[{"start":{"line":223,"column":8},"end":{"line":223,"column":62}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":223,"column":12},"end":{"line":223,"column":48}},"type":"binary-expr","locations":[{"start":{"line":223,"column":12},"end":{"line":223,"column":29}},{"start":{"line":223,"column":33},"end":{"line":223,"column":48}}]},"23":{"loc":{"start":{"line":229,"column":8},"end":{"line":229,"column":48}},"type":"if","locations":[{"start":{"line":229,"column":8},"end":{"line":229,"column":48}},{"start":{},"end":{}}]},"24":{"loc":{"start":{"line":235,"column":8},"end":{"line":235,"column":46}},"type":"if","locations":[{"start":{"line":235,"column":8},"end":{"line":235,"column":46}},{"start":{},"end":{}}]},"25":{"loc":{"start":{"line":250,"column":8},"end":{"line":250,"column":27}},"type":"if","locations":[{"start":{"line":250,"column":8},"end":{"line":250,"column":27}},{"start":{},"end":{}}]},"26":{"loc":{"start":{"line":253,"column":12},"end":{"line":254,"column":51}},"type":"binary-expr","locations":[{"start":{"line":253,"column":12},"end":{"line":253,"column":54}},{"start":{"line":254,"column":12},"end":{"line":254,"column":51}}]},"27":{"loc":{"start":{"line":256,"column":8},"end":{"line":256,"column":32}},"type":"if","locations":[{"start":{"line":256,"column":8},"end":{"line":256,"column":32}},{"start":{},"end":{}}]},"28":{"loc":{"start":{"line":261,"column":8},"end":{"line":261,"column":57}},"type":"if","locations":[{"start":{"line":261,"column":8},"end":{"line":261,"column":57}},{"start":{},"end":{}}]},"29":{"loc":{"start":{"line":261,"column":12},"end":{"line":261,"column":48}},"type":"binary-expr","locations":[{"start":{"line":261,"column":12},"end":{"line":261,"column":29}},{"start":{"line":261,"column":33},"end":{"line":261,"column":48}}]},"30":{"loc":{"start":{"line":267,"column":8},"end":{"line":267,"column":43}},"type":"if","locations":[{"start":{"line":267,"column":8},"end":{"line":267,"column":43}},{"start":{},"end":{}}]},"31":{"loc":{"start":{"line":273,"column":8},"end":{"line":273,"column":41}},"type":"if","locations":[{"start":{"line":273,"column":8},"end":{"line":273,"column":41}},{"start":{},"end":{}}]},"32":{"loc":{"start":{"line":287,"column":8},"end":{"line":297,"column":9}},"type":"if","locations":[{"start":{"line":287,"column":8},"end":{"line":297,"column":9}},{"start":{},"end":{}}]},"33":{"loc":{"start":{"line":288,"column":12},"end":{"line":289,"column":57}},"type":"binary-expr","locations":[{"start":{"line":288,"column":12},"end":{"line":288,"column":44}},{"start":{"line":289,"column":12},"end":{"line":289,"column":57}}]},"34":{"loc":{"start":{"line":299,"column":8},"end":{"line":312,"column":9}},"type":"if","locations":[{"start":{"line":299,"column":8},"end":{"line":312,"column":9}},{"start":{},"end":{}}]},"35":{"loc":{"start":{"line":301,"column":12},"end":{"line":311,"column":13}},"type":"if","locations":[{"start":{"line":301,"column":12},"end":{"line":311,"column":13}},{"start":{},"end":{}}]},"36":{"loc":{"start":{"line":302,"column":16},"end":{"line":303,"column":49}},"type":"binary-expr","locations":[{"start":{"line":302,"column":16},"end":{"line":302,"column":46}},{"start":{"line":303,"column":16},"end":{"line":303,"column":49}}]},"37":{"loc":{"start":{"line":315,"column":16},"end":{"line":315,"column":77}},"type":"cond-expr","locations":[{"start":{"line":315,"column":46},"end":{"line":315,"column":72}},{"start":{"line":315,"column":75},"end":{"line":315,"column":77}}]},"38":{"loc":{"start":{"line":316,"column":16},"end":{"line":318,"column":20}},"type":"cond-expr","locations":[{"start":{"line":317,"column":18},"end":{"line":317,"column":56}},{"start":{"line":318,"column":18},"end":{"line":318,"column":20}}]},"39":{"loc":{"start":{"line":319,"column":16},"end":{"line":321,"column":20}},"type":"cond-expr","locations":[{"start":{"line":320,"column":18},"end":{"line":320,"column":60}},{"start":{"line":321,"column":18},"end":{"line":321,"column":20}}]},"40":{"loc":{"start":{"line":322,"column":16},"end":{"line":324,"column":20}},"type":"cond-expr","locations":[{"start":{"line":323,"column":18},"end":{"line":323,"column":66}},{"start":{"line":324,"column":18},"end":{"line":324,"column":20}}]},"41":{"loc":{"start":{"line":325,"column":16},"end":{"line":325,"column":68}},"type":"cond-expr","locations":[{"start":{"line":325,"column":43},"end":{"line":325,"column":63}},{"start":{"line":325,"column":66},"end":{"line":325,"column":68}}]},"42":{"loc":{"start":{"line":326,"column":16},"end":{"line":328,"column":20}},"type":"cond-expr","locations":[{"start":{"line":327,"column":18},"end":{"line":327,"column":50}},{"start":{"line":328,"column":18},"end":{"line":328,"column":20}}]},"43":{"loc":{"start":{"line":329,"column":16},"end":{"line":329,"column":74}},"type":"cond-expr","locations":[{"start":{"line":329,"column":45},"end":{"line":329,"column":69}},{"start":{"line":329,"column":72},"end":{"line":329,"column":74}}]},"44":{"loc":{"start":{"line":330,"column":16},"end":{"line":330,"column":74}},"type":"cond-expr","locations":[{"start":{"line":330,"column":45},"end":{"line":330,"column":69}},{"start":{"line":330,"column":72},"end":{"line":330,"column":74}}]},"45":{"loc":{"start":{"line":331,"column":16},"end":{"line":333,"column":20}},"type":"cond-expr","locations":[{"start":{"line":332,"column":18},"end":{"line":332,"column":48}},{"start":{"line":333,"column":18},"end":{"line":333,"column":20}}]},"46":{"loc":{"start":{"line":334,"column":16},"end":{"line":336,"column":20}},"type":"cond-expr","locations":[{"start":{"line":335,"column":18},"end":{"line":335,"column":48}},{"start":{"line":336,"column":18},"end":{"line":336,"column":20}}]},"47":{"loc":{"start":{"line":337,"column":16},"end":{"line":339,"column":20}},"type":"cond-expr","locations":[{"start":{"line":338,"column":18},"end":{"line":338,"column":60}},{"start":{"line":339,"column":18},"end":{"line":339,"column":20}}]},"48":{"loc":{"start":{"line":340,"column":16},"end":{"line":342,"column":20}},"type":"cond-expr","locations":[{"start":{"line":341,"column":18},"end":{"line":341,"column":64}},{"start":{"line":342,"column":18},"end":{"line":342,"column":20}}]},"49":{"loc":{"start":{"line":343,"column":16},"end":{"line":345,"column":20}},"type":"cond-expr","locations":[{"start":{"line":344,"column":18},"end":{"line":344,"column":70}},{"start":{"line":345,"column":18},"end":{"line":345,"column":20}}]},"50":{"loc":{"start":{"line":346,"column":16},"end":{"line":346,"column":77}},"type":"cond-expr","locations":[{"start":{"line":346,"column":46},"end":{"line":346,"column":72}},{"start":{"line":346,"column":75},"end":{"line":346,"column":77}}]},"51":{"loc":{"start":{"line":347,"column":16},"end":{"line":349,"column":20}},"type":"cond-expr","locations":[{"start":{"line":348,"column":18},"end":{"line":348,"column":46}},{"start":{"line":349,"column":18},"end":{"line":349,"column":20}}]},"52":{"loc":{"start":{"line":355,"column":8},"end":{"line":361,"column":9}},"type":"if","locations":[{"start":{"line":355,"column":8},"end":{"line":361,"column":9}},{"start":{},"end":{}}]},"53":{"loc":{"start":{"line":356,"column":12},"end":{"line":358,"column":48}},"type":"binary-expr","locations":[{"start":{"line":356,"column":12},"end":{"line":356,"column":23}},{"start":{"line":357,"column":12},"end":{"line":357,"column":47}},{"start":{"line":358,"column":12},"end":{"line":358,"column":48}}]},"54":{"loc":{"start":{"line":363,"column":8},"end":{"line":375,"column":9}},"type":"if","locations":[{"start":{"line":363,"column":8},"end":{"line":375,"column":9}},{"start":{},"end":{}}]},"55":{"loc":{"start":{"line":364,"column":12},"end":{"line":366,"column":48}},"type":"binary-expr","locations":[{"start":{"line":364,"column":12},"end":{"line":364,"column":23}},{"start":{"line":365,"column":12},"end":{"line":365,"column":47}},{"start":{"line":366,"column":12},"end":{"line":366,"column":48}}]},"56":{"loc":{"start":{"line":384,"column":8},"end":{"line":390,"column":9}},"type":"if","locations":[{"start":{"line":384,"column":8},"end":{"line":390,"column":9}},{"start":{},"end":{}}]},"57":{"loc":{"start":{"line":398,"column":8},"end":{"line":411,"column":9}},"type":"if","locations":[{"start":{"line":398,"column":8},"end":{"line":411,"column":9}},{"start":{},"end":{}}]},"58":{"loc":{"start":{"line":400,"column":12},"end":{"line":410,"column":13}},"type":"if","locations":[{"start":{"line":400,"column":12},"end":{"line":410,"column":13}},{"start":{},"end":{}}]},"59":{"loc":{"start":{"line":401,"column":16},"end":{"line":402,"column":49}},"type":"binary-expr","locations":[{"start":{"line":401,"column":16},"end":{"line":401,"column":46}},{"start":{"line":402,"column":16},"end":{"line":402,"column":49}}]},"60":{"loc":{"start":{"line":413,"column":29},"end":{"line":415,"column":18}},"type":"cond-expr","locations":[{"start":{"line":414,"column":14},"end":{"line":414,"column":78}},{"start":{"line":415,"column":14},"end":{"line":415,"column":18}}]},"61":{"loc":{"start":{"line":417,"column":22},"end":{"line":417,"column":58}},"type":"binary-expr","locations":[{"start":{"line":417,"column":22},"end":{"line":417,"column":35}},{"start":{"line":417,"column":39},"end":{"line":417,"column":58}}]},"62":{"loc":{"start":{"line":418,"column":27},"end":{"line":418,"column":73}},"type":"binary-expr","locations":[{"start":{"line":418,"column":27},"end":{"line":418,"column":45}},{"start":{"line":418,"column":49},"end":{"line":418,"column":73}}]},"63":{"loc":{"start":{"line":420,"column":8},"end":{"line":424,"column":9}},"type":"if","locations":[{"start":{"line":420,"column":8},"end":{"line":424,"column":9}},{"start":{},"end":{}}]},"64":{"loc":{"start":{"line":420,"column":12},"end":{"line":420,"column":33}},"type":"binary-expr","locations":[{"start":{"line":420,"column":12},"end":{"line":420,"column":18}},{"start":{"line":420,"column":22},"end":{"line":420,"column":33}}]},"65":{"loc":{"start":{"line":427,"column":21},"end":{"line":427,"column":44}},"type":"binary-expr","locations":[{"start":{"line":427,"column":21},"end":{"line":427,"column":36}},{"start":{"line":427,"column":40},"end":{"line":427,"column":44}}]},"66":{"loc":{"start":{"line":428,"column":32},"end":{"line":428,"column":66}},"type":"binary-expr","locations":[{"start":{"line":428,"column":32},"end":{"line":428,"column":58}},{"start":{"line":428,"column":62},"end":{"line":428,"column":66}}]},"67":{"loc":{"start":{"line":432,"column":16},"end":{"line":432,"column":74}},"type":"binary-expr","locations":[{"start":{"line":432,"column":16},"end":{"line":432,"column":36}},{"start":{"line":432,"column":40},"end":{"line":432,"column":66}},{"start":{"line":432,"column":70},"end":{"line":432,"column":74}}]},"68":{"loc":{"start":{"line":434,"column":16},"end":{"line":436,"column":21}},"type":"binary-expr","locations":[{"start":{"line":434,"column":16},"end":{"line":434,"column":43}},{"start":{"line":435,"column":16},"end":{"line":435,"column":49}},{"start":{"line":436,"column":16},"end":{"line":436,"column":21}}]},"69":{"loc":{"start":{"line":449,"column":8},"end":{"line":453,"column":9}},"type":"if","locations":[{"start":{"line":449,"column":8},"end":{"line":453,"column":9}},{"start":{},"end":{}}]},"70":{"loc":{"start":{"line":455,"column":8},"end":{"line":468,"column":9}},"type":"if","locations":[{"start":{"line":455,"column":8},"end":{"line":468,"column":9}},{"start":{},"end":{}}]},"71":{"loc":{"start":{"line":457,"column":12},"end":{"line":467,"column":13}},"type":"if","locations":[{"start":{"line":457,"column":12},"end":{"line":467,"column":13}},{"start":{},"end":{}}]},"72":{"loc":{"start":{"line":458,"column":16},"end":{"line":459,"column":49}},"type":"binary-expr","locations":[{"start":{"line":458,"column":16},"end":{"line":458,"column":46}},{"start":{"line":459,"column":16},"end":{"line":459,"column":49}}]},"73":{"loc":{"start":{"line":478,"column":8},"end":{"line":485,"column":9}},"type":"if","locations":[{"start":{"line":478,"column":8},"end":{"line":485,"column":9}},{"start":{},"end":{}}]},"74":{"loc":{"start":{"line":498,"column":21},"end":{"line":498,"column":48}},"type":"binary-expr","locations":[{"start":{"line":498,"column":21},"end":{"line":498,"column":35}},{"start":{"line":498,"column":39},"end":{"line":498,"column":48}}]},"75":{"loc":{"start":{"line":502,"column":12},"end":{"line":512,"column":14}},"type":"binary-expr","locations":[{"start":{"line":502,"column":12},"end":{"line":502,"column":32}},{"start":{"line":503,"column":13},"end":{"line":511,"column":null}}]},"76":{"loc":{"start":{"line":505,"column":29},"end":{"line":505,"column":51}},"type":"binary-expr","locations":[{"start":{"line":505,"column":29},"end":{"line":505,"column":43}},{"start":{"line":505,"column":47},"end":{"line":505,"column":51}}]},"77":{"loc":{"start":{"line":541,"column":8},"end":{"line":548,"column":9}},"type":"if","locations":[{"start":{"line":541,"column":8},"end":{"line":548,"column":9}},{"start":{},"end":{}}]},"78":{"loc":{"start":{"line":559,"column":8},"end":{"line":569,"column":9}},"type":"if","locations":[{"start":{"line":559,"column":8},"end":{"line":569,"column":9}},{"start":{},"end":{}}]},"79":{"loc":{"start":{"line":560,"column":12},"end":{"line":561,"column":56}},"type":"binary-expr","locations":[{"start":{"line":560,"column":12},"end":{"line":560,"column":52}},{"start":{"line":561,"column":12},"end":{"line":561,"column":56}}]},"80":{"loc":{"start":{"line":576,"column":28},"end":{"line":576,"column":58}},"type":"binary-expr","locations":[{"start":{"line":576,"column":28},"end":{"line":576,"column":50}},{"start":{"line":576,"column":54},"end":{"line":576,"column":58}}]},"81":{"loc":{"start":{"line":577,"column":23},"end":{"line":577,"column":48}},"type":"binary-expr","locations":[{"start":{"line":577,"column":23},"end":{"line":577,"column":40}},{"start":{"line":577,"column":44},"end":{"line":577,"column":48}}]},"82":{"loc":{"start":{"line":612,"column":8},"end":{"line":626,"column":9}},"type":"if","locations":[{"start":{"line":612,"column":8},"end":{"line":626,"column":9}},{"start":{},"end":{}}]},"83":{"loc":{"start":{"line":613,"column":12},"end":{"line":616,"column":56}},"type":"binary-expr","locations":[{"start":{"line":613,"column":12},"end":{"line":613,"column":44}},{"start":{"line":614,"column":12},"end":{"line":614,"column":52}},{"start":{"line":615,"column":12},"end":{"line":615,"column":57}},{"start":{"line":616,"column":12},"end":{"line":616,"column":56}}]},"84":{"loc":{"start":{"line":658,"column":8},"end":{"line":668,"column":9}},"type":"if","locations":[{"start":{"line":658,"column":8},"end":{"line":668,"column":9}},{"start":{},"end":{}}]},"85":{"loc":{"start":{"line":659,"column":12},"end":{"line":660,"column":56}},"type":"binary-expr","locations":[{"start":{"line":659,"column":12},"end":{"line":659,"column":52}},{"start":{"line":660,"column":12},"end":{"line":660,"column":56}}]},"86":{"loc":{"start":{"line":675,"column":8},"end":{"line":684,"column":9}},"type":"if","locations":[{"start":{"line":675,"column":8},"end":{"line":684,"column":9}},{"start":{},"end":{}}]},"87":{"loc":{"start":{"line":701,"column":8},"end":{"line":701,"column":41}},"type":"default-arg","locations":[{"start":{"line":701,"column":26},"end":{"line":701,"column":41}}]},"88":{"loc":{"start":{"line":702,"column":8},"end":{"line":702,"column":45}},"type":"default-arg","locations":[{"start":{"line":702,"column":28},"end":{"line":702,"column":45}}]},"89":{"loc":{"start":{"line":703,"column":8},"end":{"line":703,"column":69}},"type":"default-arg","locations":[{"start":{"line":703,"column":25},"end":{"line":703,"column":69}}]},"90":{"loc":{"start":{"line":707,"column":8},"end":{"line":711,"column":9}},"type":"if","locations":[{"start":{"line":707,"column":8},"end":{"line":711,"column":9}},{"start":{},"end":{}}]},"91":{"loc":{"start":{"line":716,"column":8},"end":{"line":720,"column":9}},"type":"if","locations":[{"start":{"line":716,"column":8},"end":{"line":720,"column":9}},{"start":{},"end":{}}]},"92":{"loc":{"start":{"line":750,"column":8},"end":{"line":750,"column":27}},"type":"if","locations":[{"start":{"line":750,"column":8},"end":{"line":750,"column":27}},{"start":{},"end":{}}]},"93":{"loc":{"start":{"line":754,"column":8},"end":{"line":754,"column":28}},"type":"if","locations":[{"start":{"line":754,"column":8},"end":{"line":754,"column":28}},{"start":{},"end":{}}]},"94":{"loc":{"start":{"line":758,"column":8},"end":{"line":758,"column":57}},"type":"if","locations":[{"start":{"line":758,"column":8},"end":{"line":758,"column":57}},{"start":{},"end":{}}]},"95":{"loc":{"start":{"line":774,"column":8},"end":{"line":778,"column":9}},"type":"if","locations":[{"start":{"line":774,"column":8},"end":{"line":778,"column":9}},{"start":{},"end":{}}]},"96":{"loc":{"start":{"line":820,"column":8},"end":{"line":820,"column":27}},"type":"if","locations":[{"start":{"line":820,"column":8},"end":{"line":820,"column":27}},{"start":{},"end":{}}]},"97":{"loc":{"start":{"line":823,"column":12},"end":{"line":824,"column":51}},"type":"binary-expr","locations":[{"start":{"line":823,"column":12},"end":{"line":823,"column":54}},{"start":{"line":824,"column":12},"end":{"line":824,"column":51}}]},"98":{"loc":{"start":{"line":826,"column":8},"end":{"line":826,"column":32}},"type":"if","locations":[{"start":{"line":826,"column":8},"end":{"line":826,"column":32}},{"start":{},"end":{}}]},"99":{"loc":{"start":{"line":833,"column":8},"end":{"line":833,"column":57}},"type":"if","locations":[{"start":{"line":833,"column":8},"end":{"line":833,"column":57}},{"start":{},"end":{}}]},"100":{"loc":{"start":{"line":833,"column":12},"end":{"line":833,"column":48}},"type":"binary-expr","locations":[{"start":{"line":833,"column":12},"end":{"line":833,"column":29}},{"start":{"line":833,"column":33},"end":{"line":833,"column":48}}]},"101":{"loc":{"start":{"line":839,"column":8},"end":{"line":839,"column":43}},"type":"if","locations":[{"start":{"line":839,"column":8},"end":{"line":839,"column":43}},{"start":{},"end":{}}]},"102":{"loc":{"start":{"line":845,"column":8},"end":{"line":845,"column":41}},"type":"if","locations":[{"start":{"line":845,"column":8},"end":{"line":845,"column":41}},{"start":{},"end":{}}]},"103":{"loc":{"start":{"line":861,"column":8},"end":{"line":863,"column":9}},"type":"if","locations":[{"start":{"line":861,"column":8},"end":{"line":863,"column":9}},{"start":{},"end":{}}]},"104":{"loc":{"start":{"line":866,"column":21},"end":{"line":866,"column":44}},"type":"binary-expr","locations":[{"start":{"line":866,"column":21},"end":{"line":866,"column":36}},{"start":{"line":866,"column":40},"end":{"line":866,"column":44}}]},"105":{"loc":{"start":{"line":871,"column":26},"end":{"line":871,"column":51}},"type":"binary-expr","locations":[{"start":{"line":871,"column":26},"end":{"line":871,"column":46}},{"start":{"line":871,"column":50},"end":{"line":871,"column":51}}]},"106":{"loc":{"start":{"line":910,"column":8},"end":{"line":910,"column":41}},"type":"default-arg","locations":[{"start":{"line":910,"column":26},"end":{"line":910,"column":41}}]},"107":{"loc":{"start":{"line":911,"column":8},"end":{"line":911,"column":52}},"type":"default-arg","locations":[{"start":{"line":911,"column":35},"end":{"line":911,"column":52}}]},"108":{"loc":{"start":{"line":920,"column":8},"end":{"line":924,"column":9}},"type":"if","locations":[{"start":{"line":920,"column":8},"end":{"line":924,"column":9}},{"start":{},"end":{}}]},"109":{"loc":{"start":{"line":939,"column":29},"end":{"line":939,"column":44}},"type":"binary-expr","locations":[{"start":{"line":939,"column":29},"end":{"line":939,"column":36}},{"start":{"line":939,"column":40},"end":{"line":939,"column":44}}]},"110":{"loc":{"start":{"line":984,"column":16},"end":{"line":985,"column":63}},"type":"binary-expr","locations":[{"start":{"line":984,"column":16},"end":{"line":984,"column":59}},{"start":{"line":985,"column":16},"end":{"line":985,"column":63}}]},"111":{"loc":{"start":{"line":990,"column":8},"end":{"line":1005,"column":9}},"type":"if","locations":[{"start":{"line":990,"column":8},"end":{"line":1005,"column":9}},{"start":{"line":998,"column":15},"end":{"line":1005,"column":9}}]},"112":{"loc":{"start":{"line":990,"column":12},"end":{"line":990,"column":58}},"type":"binary-expr","locations":[{"start":{"line":990,"column":12},"end":{"line":990,"column":32}},{"start":{"line":990,"column":36},"end":{"line":990,"column":58}}]},"113":{"loc":{"start":{"line":93,"column":34},"end":{"line":93,"column":null}},"type":"cond-expr","locations":[{"start":{"line":93,"column":54},"end":{"line":93,"column":null}},{"start":{"line":93,"column":54},"end":{"line":93,"column":null}}]},"114":{"loc":{"start":{"line":93,"column":34},"end":{"line":93,"column":54}},"type":"binary-expr","locations":[{"start":{"line":93,"column":34},"end":{"line":93,"column":null}},{"start":{"line":93,"column":34},"end":{"line":93,"column":54}}]},"115":{"loc":{"start":{"line":93,"column":54},"end":{"line":95,"column":null}},"type":"cond-expr","locations":[{"start":{"line":95,"column":58},"end":{"line":95,"column":null}},{"start":{"line":95,"column":58},"end":{"line":95,"column":null}}]},"116":{"loc":{"start":{"line":93,"column":54},"end":{"line":95,"column":58}},"type":"binary-expr","locations":[{"start":{"line":93,"column":54},"end":{"line":95,"column":null}},{"start":{"line":95,"column":36},"end":{"line":95,"column":58}}]},"117":{"loc":{"start":{"line":95,"column":58},"end":{"line":97,"column":null}},"type":"cond-expr","locations":[{"start":{"line":97,"column":80},"end":{"line":97,"column":null}},{"start":{"line":97,"column":80},"end":{"line":97,"column":null}}]},"118":{"loc":{"start":{"line":95,"column":58},"end":{"line":97,"column":80}},"type":"binary-expr","locations":[{"start":{"line":95,"column":58},"end":{"line":97,"column":null}},{"start":{"line":97,"column":44},"end":{"line":97,"column":80}}]},"119":{"loc":{"start":{"line":97,"column":80},"end":{"line":99,"column":null}},"type":"cond-expr","locations":[{"start":{"line":99,"column":54},"end":{"line":99,"column":null}},{"start":{"line":99,"column":54},"end":{"line":99,"column":null}}]},"120":{"loc":{"start":{"line":97,"column":80},"end":{"line":99,"column":54}},"type":"binary-expr","locations":[{"start":{"line":97,"column":80},"end":{"line":99,"column":null}},{"start":{"line":99,"column":34},"end":{"line":99,"column":54}}]},"121":{"loc":{"start":{"line":99,"column":54},"end":{"line":101,"column":null}},"type":"cond-expr","locations":[{"start":{"line":101,"column":62},"end":{"line":101,"column":null}},{"start":{"line":101,"column":62},"end":{"line":101,"column":null}}]},"122":{"loc":{"start":{"line":99,"column":54},"end":{"line":101,"column":62}},"type":"binary-expr","locations":[{"start":{"line":99,"column":54},"end":{"line":101,"column":null}},{"start":{"line":101,"column":38},"end":{"line":101,"column":62}}]},"123":{"loc":{"start":{"line":101,"column":62},"end":{"line":103,"column":null}},"type":"cond-expr","locations":[{"start":{"line":103,"column":58},"end":{"line":103,"column":null}},{"start":{"line":103,"column":58},"end":{"line":103,"column":null}}]},"124":{"loc":{"start":{"line":101,"column":62},"end":{"line":103,"column":58}},"type":"binary-expr","locations":[{"start":{"line":101,"column":62},"end":{"line":103,"column":null}},{"start":{"line":103,"column":36},"end":{"line":103,"column":58}}]},"125":{"loc":{"start":{"line":103,"column":58},"end":{"line":105,"column":null}},"type":"cond-expr","locations":[{"start":{"line":105,"column":66},"end":{"line":105,"column":null}},{"start":{"line":105,"column":66},"end":{"line":105,"column":null}}]},"126":{"loc":{"start":{"line":103,"column":58},"end":{"line":105,"column":66}},"type":"binary-expr","locations":[{"start":{"line":103,"column":58},"end":{"line":105,"column":null}},{"start":{"line":105,"column":40},"end":{"line":105,"column":66}}]},"127":{"loc":{"start":{"line":105,"column":66},"end":{"line":107,"column":null}},"type":"cond-expr","locations":[{"start":{"line":107,"column":71},"end":{"line":107,"column":null}},{"start":{"line":107,"column":71},"end":{"line":107,"column":null}}]},"128":{"loc":{"start":{"line":105,"column":66},"end":{"line":107,"column":71}},"type":"binary-expr","locations":[{"start":{"line":105,"column":66},"end":{"line":107,"column":null}},{"start":{"line":107,"column":39},"end":{"line":107,"column":71}}]},"129":{"loc":{"start":{"line":107,"column":71},"end":{"line":108,"column":null}},"type":"cond-expr","locations":[{"start":{"line":108,"column":46},"end":{"line":108,"column":null}},{"start":{"line":108,"column":46},"end":{"line":108,"column":null}}]},"130":{"loc":{"start":{"line":107,"column":71},"end":{"line":108,"column":46}},"type":"binary-expr","locations":[{"start":{"line":107,"column":71},"end":{"line":108,"column":null}},{"start":{"line":108,"column":33},"end":{"line":108,"column":46}}]},"131":{"loc":{"start":{"line":108,"column":46},"end":{"line":109,"column":null}},"type":"cond-expr","locations":[{"start":{"line":109,"column":67},"end":{"line":109,"column":null}},{"start":{"line":109,"column":67},"end":{"line":109,"column":null}}]},"132":{"loc":{"start":{"line":108,"column":46},"end":{"line":109,"column":67}},"type":"binary-expr","locations":[{"start":{"line":108,"column":46},"end":{"line":109,"column":null}},{"start":{"line":109,"column":44},"end":{"line":109,"column":67}}]},"133":{"loc":{"start":{"line":109,"column":67},"end":{"line":110,"column":null}},"type":"cond-expr","locations":[{"start":{"line":110,"column":55},"end":{"line":110,"column":null}},{"start":{"line":110,"column":55},"end":{"line":110,"column":null}}]},"134":{"loc":{"start":{"line":109,"column":67},"end":{"line":110,"column":55}},"type":"binary-expr","locations":[{"start":{"line":109,"column":67},"end":{"line":110,"column":null}},{"start":{"line":110,"column":38},"end":{"line":110,"column":55}}]},"135":{"loc":{"start":{"line":110,"column":55},"end":{"line":111,"column":null}},"type":"cond-expr","locations":[{"start":{"line":111,"column":45},"end":{"line":111,"column":null}},{"start":{"line":111,"column":45},"end":{"line":111,"column":null}}]},"136":{"loc":{"start":{"line":110,"column":55},"end":{"line":111,"column":45}},"type":"binary-expr","locations":[{"start":{"line":110,"column":55},"end":{"line":111,"column":null}},{"start":{"line":111,"column":33},"end":{"line":111,"column":45}}]},"137":{"loc":{"start":{"line":111,"column":45},"end":{"line":112,"column":null}},"type":"cond-expr","locations":[{"start":{"line":112,"column":52},"end":{"line":112,"column":null}},{"start":{"line":112,"column":52},"end":{"line":112,"column":null}}]},"138":{"loc":{"start":{"line":111,"column":45},"end":{"line":112,"column":52}},"type":"binary-expr","locations":[{"start":{"line":111,"column":45},"end":{"line":112,"column":null}},{"start":{"line":112,"column":39},"end":{"line":112,"column":52}}]}},"s":{"0":7,"1":7,"2":7,"3":7,"4":7,"5":7,"6":7,"7":7,"8":7,"9":7,"10":7,"11":7,"12":7,"13":7,"14":7,"15":7,"16":7,"17":7,"18":7,"19":7,"20":7,"21":7,"22":7,"23":7,"24":7,"25":7,"26":7,"27":7,"28":65,"29":65,"30":65,"31":65,"32":65,"33":65,"34":65,"35":65,"36":65,"37":65,"38":65,"39":65,"40":65,"41":2,"42":1,"43":2,"44":2,"45":2,"46":2,"47":2,"48":4,"49":4,"50":4,"51":2,"52":48,"53":48,"54":1,"55":47,"56":46,"57":6,"58":0,"59":6,"60":6,"61":2,"62":4,"63":4,"64":4,"65":2,"66":2,"67":2,"68":1,"69":1,"70":5,"71":0,"72":5,"73":5,"74":3,"75":2,"76":2,"77":2,"78":1,"79":1,"80":1,"81":0,"82":1,"83":1,"84":0,"85":1,"86":47,"87":38,"88":9,"89":9,"90":4,"91":5,"92":5,"93":5,"94":2,"95":3,"96":3,"97":1,"98":2,"99":2,"100":1,"101":1,"102":4,"103":4,"104":1,"105":3,"106":1,"107":1,"108":1,"109":2,"110":2,"111":2,"112":0,"113":2,"114":1,"115":2,"116":2,"117":2,"118":2,"119":1,"120":1,"121":3,"122":2,"123":2,"124":1,"125":2,"126":2,"127":2,"128":2,"129":1,"130":1,"131":1,"132":1,"133":3,"134":3,"135":1,"136":2,"137":1,"138":1,"139":1,"140":1,"141":3,"142":3,"143":1,"144":2,"145":2,"146":2,"147":2,"148":2,"149":2,"150":1,"151":1,"152":2,"153":2,"154":1,"155":1,"156":2,"157":2,"158":1,"159":1,"160":1,"161":4,"162":4,"163":3,"164":2,"165":2,"166":2,"167":2,"168":2,"169":2,"170":2,"171":1,"172":1,"173":1,"174":3,"175":2,"176":2,"177":2,"178":1,"179":1,"180":1,"181":1,"182":1,"183":1,"184":1,"185":4,"186":4,"187":4,"188":1,"189":3,"190":3,"191":3,"192":1,"193":2,"194":2,"195":3,"196":0,"197":3,"198":3,"199":0,"200":3,"201":3,"202":2,"203":1,"204":1,"205":1,"206":0,"207":1,"208":1,"209":1,"210":1,"211":1,"212":2,"213":1,"214":2,"215":0,"216":2,"217":2,"218":1,"219":1,"220":1,"221":1,"222":1,"223":0,"224":1,"225":1,"226":0,"227":1,"228":1,"229":0,"230":1,"231":1,"232":2,"233":1,"234":2,"235":2,"236":1,"237":1,"238":1,"239":1,"240":5,"241":5,"242":5,"243":5,"244":1,"245":4,"246":4,"247":4,"248":4,"249":4,"250":1,"251":2,"252":2,"253":2,"254":3,"255":2,"256":2,"257":1,"258":1,"259":1,"260":7,"261":7},"f":{"0":65,"1":2,"2":2,"3":4,"4":4,"5":48,"6":6,"7":5,"8":47,"9":4,"10":2,"11":3,"12":1,"13":3,"14":3,"15":1,"16":2,"17":2,"18":4,"19":2,"20":2,"21":2,"22":3,"23":4,"24":3,"25":1,"26":1,"27":1,"28":2,"29":2,"30":1,"31":2,"32":1,"33":1,"34":1,"35":1,"36":5,"37":4,"38":1,"39":2,"40":3,"41":1},"b":{"0":[1,1],"1":[2,2],"2":[2,2],"3":[2,2],"4":[2,2],"5":[2,2],"6":[2,2],"7":[2,1],"8":[2,2],"9":[2,2],"10":[3,1],"11":[1,47],"12":[0,6],"13":[6,6],"14":[2,4],"15":[2,2],"16":[4,3],"17":[1,1],"18":[0,5],"19":[5,5],"20":[3,2],"21":[1,1],"22":[2,2],"23":[0,1],"24":[0,1],"25":[38,9],"26":[9,8],"27":[4,5],"28":[2,3],"29":[5,4],"30":[1,2],"31":[1,1],"32":[1,3],"33":[4,1],"34":[1,2],"35":[1,0],"36":[1,1],"37":[0,2],"38":[0,2],"39":[0,2],"40":[0,2],"41":[0,2],"42":[0,2],"43":[1,1],"44":[0,2],"45":[0,2],"46":[0,2],"47":[0,2],"48":[0,2],"49":[0,2],"50":[0,2],"51":[0,2],"52":[0,2],"53":[2,1,1],"54":[1,1],"55":[2,1,1],"56":[1,1],"57":[2,1],"58":[1,1],"59":[2,2],"60":[1,1],"61":[2,2],"62":[2,2],"63":[1,1],"64":[2,1],"65":[1,0],"66":[1,0],"67":[1,1,0],"68":[1,1,0],"69":[1,2],"70":[1,1],"71":[1,0],"72":[1,1],"73":[1,2],"74":[2,0],"75":[2,1],"76":[1,0],"77":[1,1],"78":[1,1],"79":[2,1],"80":[1,0],"81":[1,1],"82":[1,1],"83":[2,2,1,1],"84":[1,1],"85":[2,1],"86":[1,0],"87":[2],"88":[2],"89":[2],"90":[1,3],"91":[1,2],"92":[0,3],"93":[0,3],"94":[2,1],"95":[0,1],"96":[0,2],"97":[2,2],"98":[1,1],"99":[0,1],"100":[1,1],"101":[0,1],"102":[0,1],"103":[1,1],"104":[2,1],"105":[2,2],"106":[1],"107":[1],"108":[1,4],"109":[4,1],"110":[3,2],"111":[1,1],"112":[2,1],"113":[0,7],"114":[7,0],"115":[0,7],"116":[7,0],"117":[0,7],"118":[7,0],"119":[0,7],"120":[7,0],"121":[0,7],"122":[7,0],"123":[0,7],"124":[7,0],"125":[0,7],"126":[7,0],"127":[0,7],"128":[7,0],"129":[7,0],"130":[7,7],"131":[7,0],"132":[7,7],"133":[7,0],"134":[7,7],"135":[7,0],"136":[7,7],"137":[7,0],"138":[7,7]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/events/user-created.event.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/events/user-created.event.ts","statementMap":{"0":{"start":{"line":8,"column":24},"end":{"line":8,"column":38}},"1":{"start":{"line":9,"column":24},"end":{"line":9,"column":37}},"2":{"start":{"line":6,"column":0},"end":{"line":6,"column":13}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":7,"column":4},"end":{"line":7,"column":null}},"loc":{"start":{"line":9,"column":37},"end":{"line":10,"column":8}}}},"branchMap":{},"s":{"0":7,"1":7,"2":18},"f":{"0":7},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/ports/role.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/ports/role.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":4,"column":75}}},"fnMap":{},"branchMap":{},"s":{"0":17},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/ports/user.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/ports/user.repository.port.ts","statementMap":{"0":{"start":{"line":8,"column":13},"end":{"line":8,"column":75}}},"fnMap":{},"branchMap":{},"s":{"0":24},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/services/identity-role.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/services/identity-role.service.ts","statementMap":{"0":{"start":{"line":2,"column":0},"end":{"line":2,"column":52}},"1":{"start":{"line":5,"column":0},"end":{"line":5,"column":73}},"2":{"start":{"line":8,"column":7},"end":{"line":21,"column":null}},"3":{"start":{"line":11,"column":25},"end":{"line":11,"column":50}},"4":{"start":{"line":15,"column":8},"end":{"line":15,"column":35}},"5":{"start":{"line":19,"column":8},"end":{"line":19,"column":45}},"6":{"start":{"line":8,"column":13},"end":{"line":8,"column":32}},"7":{"start":{"line":8,"column":13},"end":{"line":21,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":9,"column":4},"end":{"line":9,"column":null}},"loc":{"start":{"line":11,"column":50},"end":{"line":12,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":14,"column":4},"end":{"line":14,"column":9}},"loc":{"start":{"line":14,"column":14},"end":{"line":16,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":18,"column":42},"end":{"line":20,"column":5}}}},"branchMap":{},"s":{"0":2,"1":2,"2":2,"3":2,"4":1,"5":1,"6":2,"7":2},"f":{"0":2,"1":1,"2":1},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/services/identity-user.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/identity/application/services/identity-user.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":8,"column":0},"end":{"line":8,"column":null}},"2":{"start":{"line":14,"column":0},"end":{"line":14,"column":54}},"3":{"start":{"line":15,"column":0},"end":{"line":15,"column":54}},"4":{"start":{"line":16,"column":0},"end":{"line":16,"column":64}},"5":{"start":{"line":18,"column":0},"end":{"line":18,"column":73}},"6":{"start":{"line":20,"column":0},"end":{"line":20,"column":73}},"7":{"start":{"line":25,"column":7},"end":{"line":199,"column":null}},"8":{"start":{"line":28,"column":25},"end":{"line":28,"column":50}},"9":{"start":{"line":30,"column":25},"end":{"line":30,"column":50}},"10":{"start":{"line":31,"column":25},"end":{"line":31,"column":39}},"11":{"start":{"line":35,"column":21},"end":{"line":53,"column":10}},"12":{"start":{"line":55,"column":25},"end":{"line":57,"column":10}},"13":{"start":{"line":59,"column":8},"end":{"line":63,"column":9}},"14":{"start":{"line":60,"column":12},"end":{"line":62,"column":14}},"15":{"start":{"line":65,"column":24},"end":{"line":65,"column":53}},"16":{"start":{"line":67,"column":8},"end":{"line":72,"column":9}},"17":{"start":{"line":68,"column":12},"end":{"line":71,"column":14}},"18":{"start":{"line":74,"column":8},"end":{"line":74,"column":23}},"19":{"start":{"line":78,"column":8},"end":{"line":78,"column":40}},"20":{"start":{"line":85,"column":21},"end":{"line":85,"column":56}},"21":{"start":{"line":86,"column":8},"end":{"line":86,"column":65}},"22":{"start":{"line":86,"column":19},"end":{"line":86,"column":65}},"23":{"start":{"line":87,"column":8},"end":{"line":87,"column":20}},"24":{"start":{"line":94,"column":8},"end":{"line":94,"column":51}},"25":{"start":{"line":98,"column":24},"end":{"line":98,"column":46}},"26":{"start":{"line":100,"column":12},"end":{"line":102,"column":40}},"27":{"start":{"line":104,"column":25},"end":{"line":110,"column":23}},"28":{"start":{"line":112,"column":43},"end":{"line":115,"column":10}},"29":{"start":{"line":117,"column":8},"end":{"line":117,"column":50}},"30":{"start":{"line":121,"column":8},"end":{"line":121,"column":31}},"31":{"start":{"line":122,"column":8},"end":{"line":122,"column":40}},"32":{"start":{"line":129,"column":8},"end":{"line":129,"column":35}},"33":{"start":{"line":131,"column":28},"end":{"line":131,"column":54}},"34":{"start":{"line":132,"column":22},"end":{"line":132,"column":63}},"35":{"start":{"line":133,"column":8},"end":{"line":139,"column":9}},"36":{"start":{"line":134,"column":31},"end":{"line":134,"column":64}},"37":{"start":{"line":134,"column":56},"end":{"line":134,"column":62}},"38":{"start":{"line":135,"column":28},"end":{"line":135,"column":73}},"39":{"start":{"line":135,"column":54},"end":{"line":135,"column":72}},"40":{"start":{"line":136,"column":12},"end":{"line":138,"column":14}},"41":{"start":{"line":141,"column":8},"end":{"line":141,"column":60}},"42":{"start":{"line":151,"column":25},"end":{"line":153,"column":10}},"43":{"start":{"line":155,"column":8},"end":{"line":157,"column":9}},"44":{"start":{"line":156,"column":12},"end":{"line":156,"column":28}},"45":{"start":{"line":159,"column":28},"end":{"line":175,"column":10}},"46":{"start":{"line":177,"column":26},"end":{"line":177,"column":62}},"47":{"start":{"line":179,"column":8},"end":{"line":184,"column":9}},"48":{"start":{"line":180,"column":12},"end":{"line":183,"column":14}},"49":{"start":{"line":186,"column":8},"end":{"line":186,"column":25}},"50":{"start":{"line":194,"column":22},"end":{"line":195,"column":null}},"51":{"start":{"line":197,"column":8},"end":{"line":197,"column":31}},"52":{"start":{"line":25,"column":13},"end":{"line":25,"column":32}},"53":{"start":{"line":25,"column":13},"end":{"line":199,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":26,"column":4},"end":{"line":26,"column":null}},"loc":{"start":{"line":31,"column":52},"end":{"line":32,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":34,"column":4},"end":{"line":34,"column":9}},"loc":{"start":{"line":34,"column":43},"end":{"line":75,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":77,"column":4},"end":{"line":77,"column":9}},"loc":{"start":{"line":77,"column":39},"end":{"line":79,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":81,"column":4},"end":{"line":81,"column":9}},"loc":{"start":{"line":83,"column":38},"end":{"line":88,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":90,"column":4},"end":{"line":90,"column":9}},"loc":{"start":{"line":92,"column":38},"end":{"line":95,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":97,"column":4},"end":{"line":97,"column":9}},"loc":{"start":{"line":97,"column":53},"end":{"line":118,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":120,"column":4},"end":{"line":120,"column":9}},"loc":{"start":{"line":120,"column":27},"end":{"line":123,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":125,"column":4},"end":{"line":125,"column":9}},"loc":{"start":{"line":127,"column":29},"end":{"line":142,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":134,"column":49},"end":{"line":134,"column":50}},"loc":{"start":{"line":134,"column":56},"end":{"line":134,"column":62}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":135,"column":47},"end":{"line":135,"column":48}},"loc":{"start":{"line":135,"column":54},"end":{"line":135,"column":72}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":144,"column":4},"end":{"line":144,"column":9}},"loc":{"start":{"line":150,"column":5},"end":{"line":187,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":189,"column":12},"end":{"line":189,"column":25}},"loc":{"start":{"line":192,"column":38},"end":{"line":198,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":40,"column":16},"end":{"line":44,"column":null}},"type":"binary-expr","locations":[{"start":{"line":40,"column":16},"end":{"line":40,"column":32}},{"start":{"line":41,"column":16},"end":{"line":44,"column":null}}]},"1":{"loc":{"start":{"line":48,"column":20},"end":{"line":48,"column":59}},"type":"binary-expr","locations":[{"start":{"line":48,"column":20},"end":{"line":48,"column":34}},{"start":{"line":48,"column":38},"end":{"line":48,"column":59}}]},"2":{"loc":{"start":{"line":49,"column":24},"end":{"line":49,"column":50}},"type":"binary-expr","locations":[{"start":{"line":49,"column":24},"end":{"line":49,"column":42}},{"start":{"line":49,"column":46},"end":{"line":49,"column":50}}]},"3":{"loc":{"start":{"line":50,"column":20},"end":{"line":50,"column":42}},"type":"binary-expr","locations":[{"start":{"line":50,"column":20},"end":{"line":50,"column":34}},{"start":{"line":50,"column":38},"end":{"line":50,"column":42}}]},"4":{"loc":{"start":{"line":51,"column":23},"end":{"line":51,"column":48}},"type":"binary-expr","locations":[{"start":{"line":51,"column":23},"end":{"line":51,"column":40}},{"start":{"line":51,"column":44},"end":{"line":51,"column":48}}]},"5":{"loc":{"start":{"line":52,"column":19},"end":{"line":52,"column":38}},"type":"binary-expr","locations":[{"start":{"line":52,"column":19},"end":{"line":52,"column":32}},{"start":{"line":52,"column":36},"end":{"line":52,"column":38}}]},"6":{"loc":{"start":{"line":59,"column":8},"end":{"line":63,"column":9}},"type":"if","locations":[{"start":{"line":59,"column":8},"end":{"line":63,"column":9}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":67,"column":8},"end":{"line":72,"column":9}},"type":"if","locations":[{"start":{"line":67,"column":8},"end":{"line":72,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":86,"column":8},"end":{"line":86,"column":65}},"type":"if","locations":[{"start":{"line":86,"column":8},"end":{"line":86,"column":65}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":100,"column":12},"end":{"line":102,"column":40}},"type":"binary-expr","locations":[{"start":{"line":100,"column":12},"end":{"line":100,"column":41}},{"start":{"line":101,"column":12},"end":{"line":101,"column":42}},{"start":{"line":102,"column":12},"end":{"line":102,"column":40}}]},"10":{"loc":{"start":{"line":104,"column":25},"end":{"line":110,"column":23}},"type":"cond-expr","locations":[{"start":{"line":105,"column":14},"end":{"line":108,"column":null}},{"start":{"line":110,"column":14},"end":{"line":110,"column":23}}]},"11":{"loc":{"start":{"line":106,"column":18},"end":{"line":106,"column":52}},"type":"binary-expr","locations":[{"start":{"line":106,"column":18},"end":{"line":106,"column":32}},{"start":{"line":106,"column":36},"end":{"line":106,"column":52}}]},"12":{"loc":{"start":{"line":107,"column":18},"end":{"line":107,"column":54}},"type":"binary-expr","locations":[{"start":{"line":107,"column":18},"end":{"line":107,"column":33}},{"start":{"line":107,"column":37},"end":{"line":107,"column":54}}]},"13":{"loc":{"start":{"line":108,"column":18},"end":{"line":108,"column":56}},"type":"binary-expr","locations":[{"start":{"line":108,"column":18},"end":{"line":108,"column":34}},{"start":{"line":108,"column":38},"end":{"line":108,"column":56}}]},"14":{"loc":{"start":{"line":114,"column":16},"end":{"line":114,"column":58}},"type":"cond-expr","locations":[{"start":{"line":114,"column":41},"end":{"line":114,"column":53}},{"start":{"line":114,"column":56},"end":{"line":114,"column":58}}]},"15":{"loc":{"start":{"line":133,"column":8},"end":{"line":139,"column":9}},"type":"if","locations":[{"start":{"line":133,"column":8},"end":{"line":139,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":155,"column":8},"end":{"line":157,"column":9}},"type":"if","locations":[{"start":{"line":155,"column":8},"end":{"line":157,"column":9}},{"start":{},"end":{}}]},"17":{"loc":{"start":{"line":179,"column":8},"end":{"line":184,"column":9}},"type":"if","locations":[{"start":{"line":179,"column":8},"end":{"line":184,"column":9}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":194,"column":44},"end":{"line":194,"column":67}},"type":"binary-expr","locations":[{"start":{"line":194,"column":44},"end":{"line":194,"column":54}},{"start":{"line":194,"column":58},"end":{"line":194,"column":67}}]},"19":{"loc":{"start":{"line":31,"column":39},"end":{"line":31,"column":null}},"type":"cond-expr","locations":[{"start":{"line":31,"column":52},"end":{"line":31,"column":null}},{"start":{"line":31,"column":52},"end":{"line":31,"column":null}}]},"20":{"loc":{"start":{"line":31,"column":39},"end":{"line":31,"column":52}},"type":"binary-expr","locations":[{"start":{"line":31,"column":39},"end":{"line":31,"column":null}},{"start":{"line":31,"column":39},"end":{"line":31,"column":52}}]}},"s":{"0":16,"1":16,"2":16,"3":16,"4":16,"5":16,"6":16,"7":16,"8":19,"9":19,"10":19,"11":5,"12":5,"13":5,"14":1,"15":4,"16":4,"17":3,"18":4,"19":1,"20":10,"21":10,"22":3,"23":7,"24":1,"25":3,"26":2,"27":2,"28":2,"29":2,"30":2,"31":1,"32":2,"33":2,"34":2,"35":2,"36":1,"37":1,"38":1,"39":2,"40":1,"41":1,"42":2,"43":2,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":6,"51":6,"52":16,"53":16},"f":{"0":19,"1":5,"2":1,"3":10,"4":1,"5":3,"6":2,"7":2,"8":1,"9":2,"10":2,"11":6},"b":{"0":[5,4],"1":[5,5],"2":[5,5],"3":[5,5],"4":[5,5],"5":[5,5],"6":[1,4],"7":[3,1],"8":[3,7],"9":[2,1,1],"10":[1,1],"11":[1,1],"12":[1,0],"13":[1,1],"14":[1,1],"15":[1,1],"16":[1,1],"17":[1,0],"18":[6,5],"19":[16,0],"20":[16,16]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/cluster.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/cluster.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":4,"column":71}}},"fnMap":{},"branchMap":{},"s":{"0":11},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/competence-question-template-relation.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/competence-question-template-relation.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":16},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/competence.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/competence.repository.port.ts","statementMap":{"0":{"start":{"line":7,"column":13},"end":{"line":7,"column":77}}},"fnMap":{},"branchMap":{},"s":{"0":21},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/position-competence-relation.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/position-competence-relation.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":19},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/position-question-template-relation.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/position-question-template-relation.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":9},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/question-template.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/ports/question-template.repository.port.ts","statementMap":{"0":{"start":{"line":7,"column":13},"end":{"line":9,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":16},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/cluster.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/cluster.service.ts","statementMap":{"0":{"start":{"line":6,"column":0},"end":{"line":6,"column":null}},"1":{"start":{"line":12,"column":0},"end":{"line":12,"column":60}},"2":{"start":{"line":13,"column":0},"end":{"line":13,"column":null}},"3":{"start":{"line":17,"column":0},"end":{"line":17,"column":57}},"4":{"start":{"line":20,"column":7},"end":{"line":99,"column":null}},"5":{"start":{"line":23,"column":25},"end":{"line":23,"column":35}},"6":{"start":{"line":24,"column":25},"end":{"line":24,"column":38}},"7":{"start":{"line":28,"column":8},"end":{"line":28,"column":72}},"8":{"start":{"line":29,"column":8},"end":{"line":29,"column":61}},"9":{"start":{"line":31,"column":24},"end":{"line":37,"column":10}},"10":{"start":{"line":39,"column":8},"end":{"line":39,"column":45}},"11":{"start":{"line":43,"column":8},"end":{"line":43,"column":43}},"12":{"start":{"line":47,"column":24},"end":{"line":47,"column":56}},"13":{"start":{"line":48,"column":8},"end":{"line":48,"column":71}},"14":{"start":{"line":48,"column":22},"end":{"line":48,"column":71}},"15":{"start":{"line":49,"column":8},"end":{"line":49,"column":23}},"16":{"start":{"line":56,"column":24},"end":{"line":56,"column":46}},"17":{"start":{"line":58,"column":8},"end":{"line":60,"column":9}},"18":{"start":{"line":59,"column":12},"end":{"line":59,"column":63}},"19":{"start":{"line":62,"column":8},"end":{"line":66,"column":9}},"20":{"start":{"line":63,"column":26},"end":{"line":63,"column":64}},"21":{"start":{"line":64,"column":26},"end":{"line":64,"column":64}},"22":{"start":{"line":65,"column":12},"end":{"line":65,"column":50}},"23":{"start":{"line":68,"column":46},"end":{"line":82,"column":10}},"24":{"start":{"line":84,"column":8},"end":{"line":84,"column":53}},"25":{"start":{"line":88,"column":8},"end":{"line":88,"column":31}},"26":{"start":{"line":89,"column":8},"end":{"line":89,"column":43}},"27":{"start":{"line":93,"column":8},"end":{"line":97,"column":9}},"28":{"start":{"line":94,"column":12},"end":{"line":96,"column":14}},"29":{"start":{"line":20,"column":13},"end":{"line":20,"column":27}},"30":{"start":{"line":20,"column":13},"end":{"line":99,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"loc":{"start":{"line":24,"column":55},"end":{"line":25,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":27,"column":4},"end":{"line":27,"column":9}},"loc":{"start":{"line":27,"column":46},"end":{"line":40,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":42,"column":4},"end":{"line":42,"column":9}},"loc":{"start":{"line":42,"column":42},"end":{"line":44,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":46,"column":4},"end":{"line":46,"column":9}},"loc":{"start":{"line":46,"column":28},"end":{"line":50,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":52,"column":4},"end":{"line":52,"column":9}},"loc":{"start":{"line":54,"column":35},"end":{"line":85,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":87,"column":4},"end":{"line":87,"column":9}},"loc":{"start":{"line":87,"column":27},"end":{"line":90,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":92,"column":12},"end":{"line":92,"column":17}},"loc":{"start":{"line":92,"column":59},"end":{"line":98,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":48,"column":8},"end":{"line":48,"column":71}},"type":"if","locations":[{"start":{"line":48,"column":8},"end":{"line":48,"column":71}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":58,"column":8},"end":{"line":60,"column":9}},"type":"if","locations":[{"start":{"line":58,"column":8},"end":{"line":60,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":58,"column":12},"end":{"line":58,"column":77}},"type":"binary-expr","locations":[{"start":{"line":58,"column":12},"end":{"line":58,"column":30}},{"start":{"line":58,"column":34},"end":{"line":58,"column":77}}]},"3":{"loc":{"start":{"line":62,"column":8},"end":{"line":66,"column":9}},"type":"if","locations":[{"start":{"line":62,"column":8},"end":{"line":66,"column":9}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":62,"column":12},"end":{"line":62,"column":76}},"type":"binary-expr","locations":[{"start":{"line":62,"column":12},"end":{"line":62,"column":42}},{"start":{"line":62,"column":46},"end":{"line":62,"column":76}}]},"5":{"loc":{"start":{"line":63,"column":26},"end":{"line":63,"column":64}},"type":"binary-expr","locations":[{"start":{"line":63,"column":26},"end":{"line":63,"column":42}},{"start":{"line":63,"column":46},"end":{"line":63,"column":64}}]},"6":{"loc":{"start":{"line":64,"column":26},"end":{"line":64,"column":64}},"type":"binary-expr","locations":[{"start":{"line":64,"column":26},"end":{"line":64,"column":42}},{"start":{"line":64,"column":46},"end":{"line":64,"column":64}}]},"7":{"loc":{"start":{"line":69,"column":16},"end":{"line":71,"column":20}},"type":"cond-expr","locations":[{"start":{"line":70,"column":18},"end":{"line":70,"column":54}},{"start":{"line":71,"column":18},"end":{"line":71,"column":20}}]},"8":{"loc":{"start":{"line":72,"column":16},"end":{"line":74,"column":20}},"type":"cond-expr","locations":[{"start":{"line":73,"column":18},"end":{"line":73,"column":50}},{"start":{"line":74,"column":18},"end":{"line":74,"column":20}}]},"9":{"loc":{"start":{"line":75,"column":16},"end":{"line":77,"column":20}},"type":"cond-expr","locations":[{"start":{"line":76,"column":18},"end":{"line":76,"column":50}},{"start":{"line":77,"column":18},"end":{"line":77,"column":20}}]},"10":{"loc":{"start":{"line":78,"column":16},"end":{"line":78,"column":71}},"type":"cond-expr","locations":[{"start":{"line":78,"column":44},"end":{"line":78,"column":66}},{"start":{"line":78,"column":69},"end":{"line":78,"column":71}}]},"11":{"loc":{"start":{"line":79,"column":16},"end":{"line":81,"column":20}},"type":"cond-expr","locations":[{"start":{"line":80,"column":18},"end":{"line":80,"column":52}},{"start":{"line":81,"column":18},"end":{"line":81,"column":20}}]},"12":{"loc":{"start":{"line":93,"column":8},"end":{"line":97,"column":9}},"type":"if","locations":[{"start":{"line":93,"column":8},"end":{"line":97,"column":9}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":23,"column":35},"end":{"line":23,"column":null}},"type":"cond-expr","locations":[{"start":{"line":23,"column":56},"end":{"line":23,"column":null}},{"start":{"line":23,"column":56},"end":{"line":23,"column":null}}]},"14":{"loc":{"start":{"line":23,"column":35},"end":{"line":23,"column":56}},"type":"binary-expr","locations":[{"start":{"line":23,"column":35},"end":{"line":23,"column":null}},{"start":{"line":23,"column":35},"end":{"line":23,"column":56}}]},"15":{"loc":{"start":{"line":23,"column":56},"end":{"line":24,"column":null}},"type":"cond-expr","locations":[{"start":{"line":24,"column":55},"end":{"line":24,"column":null}},{"start":{"line":24,"column":55},"end":{"line":24,"column":null}}]},"16":{"loc":{"start":{"line":23,"column":56},"end":{"line":24,"column":55}},"type":"binary-expr","locations":[{"start":{"line":23,"column":56},"end":{"line":24,"column":null}},{"start":{"line":24,"column":38},"end":{"line":24,"column":55}}]}},"s":{"0":5,"1":5,"2":5,"3":5,"4":5,"5":12,"6":12,"7":2,"8":1,"9":1,"10":1,"11":1,"12":9,"13":9,"14":3,"15":6,"16":5,"17":4,"18":1,"19":4,"20":1,"21":1,"22":1,"23":3,"24":3,"25":2,"26":1,"27":3,"28":2,"29":5,"30":5},"f":{"0":12,"1":2,"2":1,"3":9,"4":5,"5":2,"6":3},"b":{"0":[3,6],"1":[1,3],"2":[4,2],"3":[1,3],"4":[4,3],"5":[1,0],"6":[1,1],"7":[2,1],"8":[0,3],"9":[0,3],"10":[1,2],"11":[0,3],"12":[2,1],"13":[0,5],"14":[5,0],"15":[5,0],"16":[5,5]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/competence.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/competence.service.ts","statementMap":{"0":{"start":{"line":6,"column":0},"end":{"line":6,"column":null}},"1":{"start":{"line":12,"column":0},"end":{"line":12,"column":98}},"2":{"start":{"line":13,"column":0},"end":{"line":13,"column":66}},"3":{"start":{"line":14,"column":0},"end":{"line":14,"column":null}},"4":{"start":{"line":18,"column":0},"end":{"line":18,"column":null}},"5":{"start":{"line":22,"column":0},"end":{"line":22,"column":null}},"6":{"start":{"line":26,"column":0},"end":{"line":26,"column":null}},"7":{"start":{"line":32,"column":7},"end":{"line":172,"column":null}},"8":{"start":{"line":35,"column":25},"end":{"line":35,"column":38}},"9":{"start":{"line":37,"column":25},"end":{"line":37,"column":44}},"10":{"start":{"line":39,"column":25},"end":{"line":39,"column":54}},"11":{"start":{"line":41,"column":25},"end":{"line":41,"column":62}},"12":{"start":{"line":42,"column":25},"end":{"line":42,"column":36}},"13":{"start":{"line":46,"column":27},"end":{"line":50,"column":10}},"14":{"start":{"line":51,"column":8},"end":{"line":51,"column":51}},"15":{"start":{"line":55,"column":8},"end":{"line":55,"column":46}},"16":{"start":{"line":59,"column":27},"end":{"line":59,"column":62}},"17":{"start":{"line":60,"column":8},"end":{"line":60,"column":77}},"18":{"start":{"line":60,"column":25},"end":{"line":60,"column":77}},"19":{"start":{"line":61,"column":8},"end":{"line":61,"column":26}},"20":{"start":{"line":68,"column":8},"end":{"line":68,"column":31}},"21":{"start":{"line":69,"column":49},"end":{"line":75,"column":10}},"22":{"start":{"line":76,"column":8},"end":{"line":76,"column":56}},"23":{"start":{"line":80,"column":8},"end":{"line":80,"column":31}},"24":{"start":{"line":81,"column":41},"end":{"line":81,"column":77}},"25":{"start":{"line":82,"column":33},"end":{"line":82,"column":61}},"26":{"start":{"line":83,"column":8},"end":{"line":96,"column":9}},"27":{"start":{"line":87,"column":12},"end":{"line":95,"column":14}},"28":{"start":{"line":97,"column":8},"end":{"line":97,"column":74}},"29":{"start":{"line":98,"column":8},"end":{"line":100,"column":10}},"30":{"start":{"line":101,"column":8},"end":{"line":101,"column":46}},"31":{"start":{"line":108,"column":8},"end":{"line":108,"column":41}},"32":{"start":{"line":109,"column":8},"end":{"line":109,"column":49}},"33":{"start":{"line":111,"column":8},"end":{"line":111,"column":78}},"34":{"start":{"line":112,"column":8},"end":{"line":112,"column":48}},"35":{"start":{"line":119,"column":8},"end":{"line":119,"column":41}},"36":{"start":{"line":120,"column":8},"end":{"line":120,"column":80}},"37":{"start":{"line":124,"column":8},"end":{"line":124,"column":41}},"38":{"start":{"line":126,"column":12},"end":{"line":127,"column":null}},"39":{"start":{"line":129,"column":8},"end":{"line":129,"column":50}},"40":{"start":{"line":129,"column":36},"end":{"line":129,"column":48}},"41":{"start":{"line":136,"column":8},"end":{"line":136,"column":41}},"42":{"start":{"line":137,"column":8},"end":{"line":137,"column":68}},"43":{"start":{"line":139,"column":8},"end":{"line":142,"column":10}},"44":{"start":{"line":143,"column":8},"end":{"line":143,"column":56}},"45":{"start":{"line":150,"column":8},"end":{"line":150,"column":41}},"46":{"start":{"line":151,"column":8},"end":{"line":154,"column":10}},"47":{"start":{"line":158,"column":8},"end":{"line":158,"column":41}},"48":{"start":{"line":160,"column":12},"end":{"line":161,"column":null}},"49":{"start":{"line":163,"column":8},"end":{"line":163,"column":58}},"50":{"start":{"line":163,"column":36},"end":{"line":163,"column":56}},"51":{"start":{"line":169,"column":22},"end":{"line":169,"column":79}},"52":{"start":{"line":170,"column":8},"end":{"line":170,"column":79}},"53":{"start":{"line":170,"column":20},"end":{"line":170,"column":79}},"54":{"start":{"line":32,"column":13},"end":{"line":32,"column":30}},"55":{"start":{"line":32,"column":13},"end":{"line":172,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":33,"column":4},"end":{"line":33,"column":null}},"loc":{"start":{"line":42,"column":51},"end":{"line":43,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":45,"column":4},"end":{"line":45,"column":9}},"loc":{"start":{"line":45,"column":49},"end":{"line":52,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":54,"column":4},"end":{"line":54,"column":9}},"loc":{"start":{"line":54,"column":45},"end":{"line":56,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":58,"column":4},"end":{"line":58,"column":9}},"loc":{"start":{"line":58,"column":28},"end":{"line":62,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":64,"column":4},"end":{"line":64,"column":9}},"loc":{"start":{"line":66,"column":38},"end":{"line":77,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":79,"column":4},"end":{"line":79,"column":9}},"loc":{"start":{"line":79,"column":27},"end":{"line":102,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":104,"column":4},"end":{"line":104,"column":9}},"loc":{"start":{"line":106,"column":26},"end":{"line":113,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":115,"column":4},"end":{"line":115,"column":9}},"loc":{"start":{"line":117,"column":26},"end":{"line":121,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":123,"column":4},"end":{"line":123,"column":9}},"loc":{"start":{"line":123,"column":44},"end":{"line":130,"column":5}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":129,"column":29},"end":{"line":129,"column":30}},"loc":{"start":{"line":129,"column":36},"end":{"line":129,"column":48}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":132,"column":4},"end":{"line":132,"column":9}},"loc":{"start":{"line":134,"column":34},"end":{"line":144,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":146,"column":4},"end":{"line":146,"column":9}},"loc":{"start":{"line":148,"column":34},"end":{"line":155,"column":5}}},"12":{"name":"(anonymous_16)","decl":{"start":{"line":157,"column":4},"end":{"line":157,"column":9}},"loc":{"start":{"line":157,"column":52},"end":{"line":164,"column":5}}},"13":{"name":"(anonymous_17)","decl":{"start":{"line":163,"column":29},"end":{"line":163,"column":30}},"loc":{"start":{"line":163,"column":36},"end":{"line":163,"column":56}}},"14":{"name":"(anonymous_18)","decl":{"start":{"line":166,"column":12},"end":{"line":166,"column":17}},"loc":{"start":{"line":167,"column":34},"end":{"line":171,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":47,"column":18},"end":{"line":47,"column":38}},"type":"binary-expr","locations":[{"start":{"line":47,"column":18},"end":{"line":47,"column":30}},{"start":{"line":47,"column":34},"end":{"line":47,"column":38}}]},"1":{"loc":{"start":{"line":49,"column":25},"end":{"line":49,"column":52}},"type":"binary-expr","locations":[{"start":{"line":49,"column":25},"end":{"line":49,"column":44}},{"start":{"line":49,"column":48},"end":{"line":49,"column":52}}]},"2":{"loc":{"start":{"line":60,"column":8},"end":{"line":60,"column":77}},"type":"if","locations":[{"start":{"line":60,"column":8},"end":{"line":60,"column":77}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":70,"column":16},"end":{"line":70,"column":68}},"type":"cond-expr","locations":[{"start":{"line":70,"column":43},"end":{"line":70,"column":63}},{"start":{"line":70,"column":66},"end":{"line":70,"column":68}}]},"4":{"loc":{"start":{"line":71,"column":16},"end":{"line":71,"column":71}},"type":"cond-expr","locations":[{"start":{"line":71,"column":44},"end":{"line":71,"column":66}},{"start":{"line":71,"column":69},"end":{"line":71,"column":71}}]},"5":{"loc":{"start":{"line":72,"column":16},"end":{"line":74,"column":20}},"type":"cond-expr","locations":[{"start":{"line":73,"column":18},"end":{"line":73,"column":52}},{"start":{"line":74,"column":18},"end":{"line":74,"column":20}}]},"6":{"loc":{"start":{"line":83,"column":8},"end":{"line":96,"column":9}},"type":"if","locations":[{"start":{"line":83,"column":8},"end":{"line":96,"column":9}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":84,"column":12},"end":{"line":85,"column":39}},"type":"binary-expr","locations":[{"start":{"line":84,"column":12},"end":{"line":84,"column":47}},{"start":{"line":85,"column":12},"end":{"line":85,"column":39}}]},"8":{"loc":{"start":{"line":170,"column":8},"end":{"line":170,"column":79}},"type":"if","locations":[{"start":{"line":170,"column":8},"end":{"line":170,"column":79}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":35,"column":38},"end":{"line":35,"column":null}},"type":"cond-expr","locations":[{"start":{"line":35,"column":62},"end":{"line":35,"column":null}},{"start":{"line":35,"column":62},"end":{"line":35,"column":null}}]},"10":{"loc":{"start":{"line":35,"column":38},"end":{"line":35,"column":62}},"type":"binary-expr","locations":[{"start":{"line":35,"column":38},"end":{"line":35,"column":null}},{"start":{"line":35,"column":38},"end":{"line":35,"column":62}}]},"11":{"loc":{"start":{"line":35,"column":62},"end":{"line":37,"column":null}},"type":"cond-expr","locations":[{"start":{"line":37,"column":74},"end":{"line":37,"column":null}},{"start":{"line":37,"column":74},"end":{"line":37,"column":null}}]},"12":{"loc":{"start":{"line":35,"column":62},"end":{"line":37,"column":74}},"type":"binary-expr","locations":[{"start":{"line":35,"column":62},"end":{"line":37,"column":null}},{"start":{"line":37,"column":44},"end":{"line":37,"column":74}}]},"13":{"loc":{"start":{"line":37,"column":74},"end":{"line":39,"column":null}},"type":"cond-expr","locations":[{"start":{"line":39,"column":94},"end":{"line":39,"column":null}},{"start":{"line":39,"column":94},"end":{"line":39,"column":null}}]},"14":{"loc":{"start":{"line":37,"column":74},"end":{"line":39,"column":94}},"type":"binary-expr","locations":[{"start":{"line":37,"column":74},"end":{"line":39,"column":null}},{"start":{"line":39,"column":54},"end":{"line":39,"column":94}}]},"15":{"loc":{"start":{"line":39,"column":94},"end":{"line":41,"column":null}},"type":"cond-expr","locations":[{"start":{"line":41,"column":110},"end":{"line":41,"column":null}},{"start":{"line":41,"column":110},"end":{"line":41,"column":null}}]},"16":{"loc":{"start":{"line":39,"column":94},"end":{"line":41,"column":110}},"type":"binary-expr","locations":[{"start":{"line":39,"column":94},"end":{"line":41,"column":null}},{"start":{"line":41,"column":62},"end":{"line":41,"column":110}}]},"17":{"loc":{"start":{"line":41,"column":110},"end":{"line":42,"column":null}},"type":"cond-expr","locations":[{"start":{"line":42,"column":51},"end":{"line":42,"column":null}},{"start":{"line":42,"column":51},"end":{"line":42,"column":null}}]},"18":{"loc":{"start":{"line":41,"column":110},"end":{"line":42,"column":51}},"type":"binary-expr","locations":[{"start":{"line":41,"column":110},"end":{"line":42,"column":null}},{"start":{"line":42,"column":36},"end":{"line":42,"column":51}}]}},"s":{"0":16,"1":16,"2":16,"3":16,"4":16,"5":16,"6":16,"7":16,"8":17,"9":17,"10":17,"11":17,"12":17,"13":2,"14":2,"15":1,"16":22,"17":22,"18":2,"19":20,"20":2,"21":1,"22":1,"23":3,"24":3,"25":3,"26":3,"27":2,"28":1,"29":1,"30":1,"31":1,"32":1,"33":1,"34":1,"35":1,"36":1,"37":5,"38":5,"39":5,"40":4,"41":2,"42":2,"43":1,"44":1,"45":1,"46":1,"47":5,"48":5,"49":5,"50":4,"51":2,"52":2,"53":1,"54":16,"55":16},"f":{"0":17,"1":2,"2":1,"3":22,"4":2,"5":3,"6":1,"7":1,"8":5,"9":4,"10":2,"11":1,"12":5,"13":4,"14":2},"b":{"0":[2,1],"1":[2,1],"2":[2,20],"3":[0,1],"4":[1,0],"5":[0,1],"6":[2,1],"7":[3,2],"8":[1,1],"9":[0,16],"10":[16,0],"11":[0,16],"12":[16,0],"13":[0,16],"14":[16,0],"15":[0,16],"16":[16,0],"17":[16,0],"18":[16,16]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/question-template.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/library/application/services/question-template.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"2":{"start":{"line":13,"column":0},"end":{"line":13,"column":null}},"3":{"start":{"line":17,"column":0},"end":{"line":17,"column":98}},"4":{"start":{"line":18,"column":0},"end":{"line":18,"column":79}},"5":{"start":{"line":19,"column":0},"end":{"line":19,"column":null}},"6":{"start":{"line":23,"column":0},"end":{"line":23,"column":null}},"7":{"start":{"line":27,"column":0},"end":{"line":27,"column":null}},"8":{"start":{"line":31,"column":0},"end":{"line":31,"column":57}},"9":{"start":{"line":34,"column":7},"end":{"line":226,"column":null}},"10":{"start":{"line":37,"column":25},"end":{"line":37,"column":44}},"11":{"start":{"line":39,"column":25},"end":{"line":39,"column":60}},"12":{"start":{"line":41,"column":25},"end":{"line":41,"column":62}},"13":{"start":{"line":42,"column":25},"end":{"line":42,"column":38}},"14":{"start":{"line":43,"column":25},"end":{"line":43,"column":36}},"15":{"start":{"line":45,"column":25},"end":{"line":45,"column":42}},"16":{"start":{"line":51,"column":8},"end":{"line":51,"column":61}},"17":{"start":{"line":53,"column":25},"end":{"line":60,"column":10}},"18":{"start":{"line":62,"column":24},"end":{"line":62,"column":69}},"19":{"start":{"line":64,"column":28},"end":{"line":66,"column":16}},"20":{"start":{"line":67,"column":8},"end":{"line":73,"column":9}},"21":{"start":{"line":68,"column":12},"end":{"line":68,"column":57}},"22":{"start":{"line":69,"column":12},"end":{"line":72,"column":14}},"23":{"start":{"line":75,"column":8},"end":{"line":75,"column":41}},"24":{"start":{"line":81,"column":8},"end":{"line":81,"column":52}},"25":{"start":{"line":85,"column":25},"end":{"line":85,"column":66}},"26":{"start":{"line":86,"column":8},"end":{"line":87,"column":71}},"27":{"start":{"line":87,"column":12},"end":{"line":87,"column":71}},"28":{"start":{"line":88,"column":8},"end":{"line":88,"column":24}},"29":{"start":{"line":95,"column":24},"end":{"line":95,"column":46}},"30":{"start":{"line":97,"column":8},"end":{"line":99,"column":9}},"31":{"start":{"line":98,"column":12},"end":{"line":98,"column":63}},"32":{"start":{"line":101,"column":55},"end":{"line":116,"column":10}},"33":{"start":{"line":118,"column":8},"end":{"line":118,"column":61}},"34":{"start":{"line":120,"column":8},"end":{"line":127,"column":9}},"35":{"start":{"line":121,"column":36},"end":{"line":121,"column":74}},"36":{"start":{"line":122,"column":12},"end":{"line":122,"column":61}},"37":{"start":{"line":123,"column":12},"end":{"line":126,"column":14}},"38":{"start":{"line":129,"column":8},"end":{"line":129,"column":32}},"39":{"start":{"line":133,"column":8},"end":{"line":133,"column":31}},"40":{"start":{"line":134,"column":33},"end":{"line":136,"column":10}},"41":{"start":{"line":137,"column":8},"end":{"line":145,"column":9}},"42":{"start":{"line":138,"column":12},"end":{"line":144,"column":14}},"43":{"start":{"line":146,"column":8},"end":{"line":148,"column":10}},"44":{"start":{"line":149,"column":8},"end":{"line":151,"column":10}},"45":{"start":{"line":152,"column":8},"end":{"line":152,"column":52}},"46":{"start":{"line":159,"column":8},"end":{"line":159,"column":39}},"47":{"start":{"line":160,"column":8},"end":{"line":160,"column":49}},"48":{"start":{"line":162,"column":8},"end":{"line":165,"column":10}},"49":{"start":{"line":166,"column":8},"end":{"line":166,"column":46}},"50":{"start":{"line":173,"column":8},"end":{"line":173,"column":39}},"51":{"start":{"line":174,"column":8},"end":{"line":177,"column":10}},"52":{"start":{"line":181,"column":8},"end":{"line":181,"column":39}},"53":{"start":{"line":183,"column":12},"end":{"line":184,"column":null}},"54":{"start":{"line":186,"column":8},"end":{"line":186,"column":50}},"55":{"start":{"line":186,"column":36},"end":{"line":186,"column":48}},"56":{"start":{"line":193,"column":8},"end":{"line":193,"column":39}},"57":{"start":{"line":194,"column":8},"end":{"line":194,"column":53}},"58":{"start":{"line":196,"column":8},"end":{"line":199,"column":10}},"59":{"start":{"line":200,"column":8},"end":{"line":200,"column":48}},"60":{"start":{"line":207,"column":8},"end":{"line":207,"column":39}},"61":{"start":{"line":208,"column":8},"end":{"line":211,"column":10}},"62":{"start":{"line":215,"column":8},"end":{"line":215,"column":39}},"63":{"start":{"line":217,"column":12},"end":{"line":218,"column":null}},"64":{"start":{"line":220,"column":8},"end":{"line":220,"column":52}},"65":{"start":{"line":220,"column":36},"end":{"line":220,"column":50}},"66":{"start":{"line":224,"column":8},"end":{"line":224,"column":79}},"67":{"start":{"line":224,"column":50},"end":{"line":224,"column":76}},"68":{"start":{"line":34,"column":13},"end":{"line":34,"column":36}},"69":{"start":{"line":34,"column":13},"end":{"line":226,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},"loc":{"start":{"line":45,"column":64},"end":{"line":46,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":48,"column":4},"end":{"line":48,"column":9}},"loc":{"start":{"line":49,"column":46},"end":{"line":76,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":78,"column":4},"end":{"line":78,"column":9}},"loc":{"start":{"line":79,"column":42},"end":{"line":82,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":84,"column":4},"end":{"line":84,"column":9}},"loc":{"start":{"line":84,"column":28},"end":{"line":89,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":91,"column":4},"end":{"line":91,"column":9}},"loc":{"start":{"line":93,"column":44},"end":{"line":130,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":132,"column":4},"end":{"line":132,"column":9}},"loc":{"start":{"line":132,"column":27},"end":{"line":153,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":155,"column":4},"end":{"line":155,"column":9}},"loc":{"start":{"line":157,"column":26},"end":{"line":167,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":169,"column":4},"end":{"line":169,"column":9}},"loc":{"start":{"line":171,"column":26},"end":{"line":178,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":180,"column":4},"end":{"line":180,"column":9}},"loc":{"start":{"line":180,"column":42},"end":{"line":187,"column":5}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":186,"column":29},"end":{"line":186,"column":30}},"loc":{"start":{"line":186,"column":36},"end":{"line":186,"column":48}}},"10":{"name":"(anonymous_14)","decl":{"start":{"line":189,"column":4},"end":{"line":189,"column":9}},"loc":{"start":{"line":191,"column":28},"end":{"line":201,"column":5}}},"11":{"name":"(anonymous_15)","decl":{"start":{"line":203,"column":4},"end":{"line":203,"column":9}},"loc":{"start":{"line":205,"column":28},"end":{"line":212,"column":5}}},"12":{"name":"(anonymous_16)","decl":{"start":{"line":214,"column":4},"end":{"line":214,"column":9}},"loc":{"start":{"line":214,"column":44},"end":{"line":221,"column":5}}},"13":{"name":"(anonymous_17)","decl":{"start":{"line":220,"column":29},"end":{"line":220,"column":30}},"loc":{"start":{"line":220,"column":36},"end":{"line":220,"column":50}}},"14":{"name":"(anonymous_18)","decl":{"start":{"line":223,"column":12},"end":{"line":223,"column":17}},"loc":{"start":{"line":223,"column":60},"end":{"line":225,"column":5}}},"15":{"name":"(anonymous_19)","decl":{"start":{"line":224,"column":42},"end":{"line":224,"column":43}},"loc":{"start":{"line":224,"column":50},"end":{"line":224,"column":76}}}},"branchMap":{"0":{"loc":{"start":{"line":57,"column":33},"end":{"line":57,"column":69}},"type":"binary-expr","locations":[{"start":{"line":57,"column":33},"end":{"line":57,"column":60}},{"start":{"line":57,"column":64},"end":{"line":57,"column":69}}]},"1":{"loc":{"start":{"line":58,"column":20},"end":{"line":58,"column":67}},"type":"binary-expr","locations":[{"start":{"line":58,"column":20},"end":{"line":58,"column":34}},{"start":{"line":58,"column":38},"end":{"line":58,"column":67}}]},"2":{"loc":{"start":{"line":59,"column":25},"end":{"line":59,"column":50}},"type":"binary-expr","locations":[{"start":{"line":59,"column":25},"end":{"line":59,"column":44}},{"start":{"line":59,"column":48},"end":{"line":59,"column":50}}]},"3":{"loc":{"start":{"line":64,"column":28},"end":{"line":66,"column":16}},"type":"cond-expr","locations":[{"start":{"line":65,"column":14},"end":{"line":65,"column":54}},{"start":{"line":66,"column":14},"end":{"line":66,"column":16}}]},"4":{"loc":{"start":{"line":67,"column":8},"end":{"line":73,"column":9}},"type":"if","locations":[{"start":{"line":67,"column":8},"end":{"line":73,"column":9}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":86,"column":8},"end":{"line":87,"column":71}},"type":"if","locations":[{"start":{"line":86,"column":8},"end":{"line":87,"column":71}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":97,"column":8},"end":{"line":99,"column":9}},"type":"if","locations":[{"start":{"line":97,"column":8},"end":{"line":99,"column":9}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":97,"column":12},"end":{"line":97,"column":77}},"type":"binary-expr","locations":[{"start":{"line":97,"column":12},"end":{"line":97,"column":30}},{"start":{"line":97,"column":34},"end":{"line":97,"column":77}}]},"8":{"loc":{"start":{"line":102,"column":16},"end":{"line":104,"column":20}},"type":"cond-expr","locations":[{"start":{"line":103,"column":18},"end":{"line":103,"column":54}},{"start":{"line":104,"column":18},"end":{"line":104,"column":20}}]},"9":{"loc":{"start":{"line":105,"column":16},"end":{"line":105,"column":71}},"type":"cond-expr","locations":[{"start":{"line":105,"column":44},"end":{"line":105,"column":66}},{"start":{"line":105,"column":69},"end":{"line":105,"column":71}}]},"10":{"loc":{"start":{"line":106,"column":16},"end":{"line":108,"column":20}},"type":"cond-expr","locations":[{"start":{"line":107,"column":18},"end":{"line":107,"column":50}},{"start":{"line":108,"column":18},"end":{"line":108,"column":20}}]},"11":{"loc":{"start":{"line":109,"column":16},"end":{"line":111,"column":20}},"type":"cond-expr","locations":[{"start":{"line":110,"column":18},"end":{"line":110,"column":68}},{"start":{"line":111,"column":18},"end":{"line":111,"column":20}}]},"12":{"loc":{"start":{"line":112,"column":16},"end":{"line":112,"column":74}},"type":"cond-expr","locations":[{"start":{"line":112,"column":45},"end":{"line":112,"column":69}},{"start":{"line":112,"column":72},"end":{"line":112,"column":74}}]},"13":{"loc":{"start":{"line":113,"column":16},"end":{"line":115,"column":20}},"type":"cond-expr","locations":[{"start":{"line":114,"column":18},"end":{"line":114,"column":52}},{"start":{"line":115,"column":18},"end":{"line":115,"column":20}}]},"14":{"loc":{"start":{"line":120,"column":8},"end":{"line":127,"column":9}},"type":"if","locations":[{"start":{"line":120,"column":8},"end":{"line":127,"column":9}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":137,"column":8},"end":{"line":145,"column":9}},"type":"if","locations":[{"start":{"line":137,"column":8},"end":{"line":145,"column":9}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":37,"column":44},"end":{"line":37,"column":null}},"type":"cond-expr","locations":[{"start":{"line":37,"column":74},"end":{"line":37,"column":null}},{"start":{"line":37,"column":74},"end":{"line":37,"column":null}}]},"17":{"loc":{"start":{"line":37,"column":44},"end":{"line":37,"column":74}},"type":"binary-expr","locations":[{"start":{"line":37,"column":44},"end":{"line":37,"column":null}},{"start":{"line":37,"column":44},"end":{"line":37,"column":74}}]},"18":{"loc":{"start":{"line":37,"column":74},"end":{"line":39,"column":null}},"type":"cond-expr","locations":[{"start":{"line":39,"column":106},"end":{"line":39,"column":null}},{"start":{"line":39,"column":106},"end":{"line":39,"column":null}}]},"19":{"loc":{"start":{"line":37,"column":74},"end":{"line":39,"column":106}},"type":"binary-expr","locations":[{"start":{"line":37,"column":74},"end":{"line":39,"column":null}},{"start":{"line":39,"column":60},"end":{"line":39,"column":106}}]},"20":{"loc":{"start":{"line":39,"column":106},"end":{"line":41,"column":null}},"type":"cond-expr","locations":[{"start":{"line":41,"column":110},"end":{"line":41,"column":null}},{"start":{"line":41,"column":110},"end":{"line":41,"column":null}}]},"21":{"loc":{"start":{"line":39,"column":106},"end":{"line":41,"column":110}},"type":"binary-expr","locations":[{"start":{"line":39,"column":106},"end":{"line":41,"column":null}},{"start":{"line":41,"column":62},"end":{"line":41,"column":110}}]},"22":{"loc":{"start":{"line":41,"column":110},"end":{"line":42,"column":null}},"type":"cond-expr","locations":[{"start":{"line":42,"column":55},"end":{"line":42,"column":null}},{"start":{"line":42,"column":55},"end":{"line":42,"column":null}}]},"23":{"loc":{"start":{"line":41,"column":110},"end":{"line":42,"column":55}},"type":"binary-expr","locations":[{"start":{"line":41,"column":110},"end":{"line":42,"column":null}},{"start":{"line":42,"column":38},"end":{"line":42,"column":55}}]},"24":{"loc":{"start":{"line":42,"column":55},"end":{"line":43,"column":null}},"type":"cond-expr","locations":[{"start":{"line":43,"column":51},"end":{"line":43,"column":null}},{"start":{"line":43,"column":51},"end":{"line":43,"column":null}}]},"25":{"loc":{"start":{"line":42,"column":55},"end":{"line":43,"column":51}},"type":"binary-expr","locations":[{"start":{"line":42,"column":55},"end":{"line":43,"column":null}},{"start":{"line":43,"column":36},"end":{"line":43,"column":51}}]},"26":{"loc":{"start":{"line":43,"column":51},"end":{"line":45,"column":null}},"type":"cond-expr","locations":[{"start":{"line":45,"column":64},"end":{"line":45,"column":null}},{"start":{"line":45,"column":64},"end":{"line":45,"column":null}}]},"27":{"loc":{"start":{"line":43,"column":51},"end":{"line":45,"column":64}},"type":"binary-expr","locations":[{"start":{"line":43,"column":51},"end":{"line":45,"column":null}},{"start":{"line":45,"column":42},"end":{"line":45,"column":64}}]}},"s":{"0":9,"1":9,"2":9,"3":9,"4":9,"5":9,"6":9,"7":9,"8":9,"9":9,"10":16,"11":16,"12":16,"13":16,"14":16,"15":16,"16":2,"17":2,"18":2,"19":2,"20":2,"21":1,"22":1,"23":2,"24":1,"25":20,"26":20,"27":1,"28":19,"29":3,"30":3,"31":1,"32":3,"33":3,"34":3,"35":1,"36":1,"37":1,"38":3,"39":2,"40":2,"41":2,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"51":1,"52":2,"53":2,"54":2,"55":3,"56":1,"57":1,"58":1,"59":1,"60":1,"61":1,"62":2,"63":2,"64":2,"65":3,"66":2,"67":4,"68":9,"69":9},"f":{"0":16,"1":2,"2":1,"3":20,"4":3,"5":2,"6":1,"7":1,"8":2,"9":3,"10":1,"11":1,"12":2,"13":3,"14":2,"15":4},"b":{"0":[2,2],"1":[2,2],"2":[2,1],"3":[1,1],"4":[1,1],"5":[1,19],"6":[1,2],"7":[3,1],"8":[1,2],"9":[1,2],"10":[0,3],"11":[0,3],"12":[0,3],"13":[1,2],"14":[1,2],"15":[1,1],"16":[0,9],"17":[9,0],"18":[0,9],"19":[9,0],"20":[0,9],"21":[9,0],"22":[9,0],"23":[9,9],"24":[9,0],"25":[9,9],"26":[0,9],"27":[9,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/cycle-stage-notification.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/cycle-stage-notification.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":50}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":52}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":48}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":111}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":95}},"5":{"start":{"line":8,"column":43},"end":{"line":43,"column":null}},"6":{"start":{"line":9,"column":30},"end":{"line":9,"column":77}},"7":{"start":{"line":12,"column":25},"end":{"line":12,"column":40}},"8":{"start":{"line":17,"column":37},"end":{"line":17,"column":42}},"9":{"start":{"line":19,"column":8},"end":{"line":41,"column":9}},"10":{"start":{"line":20,"column":12},"end":{"line":33,"column":13}},"11":{"start":{"line":23,"column":24},"end":{"line":24,"column":null}},"12":{"start":{"line":26,"column":20},"end":{"line":28,"column":22}},"13":{"start":{"line":29,"column":20},"end":{"line":29,"column":27}},"14":{"start":{"line":32,"column":20},"end":{"line":32,"column":27}},"15":{"start":{"line":36,"column":16},"end":{"line":36,"column":70}},"16":{"start":{"line":37,"column":12},"end":{"line":40,"column":14}},"17":{"start":{"line":8,"column":13},"end":{"line":8,"column":43}},"18":{"start":{"line":16,"column":10},"end":{"line":42,"column":null}},"19":{"start":{"line":8,"column":13},"end":{"line":43,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":11,"column":4},"end":{"line":11,"column":null}},"loc":{"start":{"line":12,"column":70},"end":{"line":13,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":16,"column":10},"end":{"line":16,"column":16}},"loc":{"start":{"line":16,"column":46},"end":{"line":42,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":20,"column":12},"end":{"line":33,"column":13}},"type":"switch","locations":[{"start":{"line":21,"column":16},"end":{"line":30,"column":17}},{"start":{"line":31,"column":16},"end":{"line":32,"column":27}}]},"1":{"loc":{"start":{"line":36,"column":16},"end":{"line":36,"column":70}},"type":"cond-expr","locations":[{"start":{"line":36,"column":41},"end":{"line":36,"column":54}},{"start":{"line":36,"column":57},"end":{"line":36,"column":70}}]},"2":{"loc":{"start":{"line":39,"column":16},"end":{"line":39,"column":64}},"type":"cond-expr","locations":[{"start":{"line":39,"column":41},"end":{"line":39,"column":52}},{"start":{"line":39,"column":55},"end":{"line":39,"column":64}}]},"3":{"loc":{"start":{"line":16,"column":24},"end":{"line":16,"column":49}},"type":"cond-expr","locations":[{"start":{"line":16,"column":46},"end":{"line":16,"column":49}},{"start":{"line":16,"column":46},"end":{"line":16,"column":49}}]},"4":{"loc":{"start":{"line":16,"column":24},"end":{"line":16,"column":46}},"type":"binary-expr","locations":[{"start":{"line":16,"column":24},"end":{"line":16,"column":49}},{"start":{"line":16,"column":24},"end":{"line":16,"column":46}}]},"5":{"loc":{"start":{"line":16,"column":49},"end":{"line":16,"column":null}},"type":"cond-expr","locations":[{"start":{"line":16,"column":56},"end":{"line":16,"column":null}},{"start":{"line":16,"column":56},"end":{"line":16,"column":null}}]},"6":{"loc":{"start":{"line":16,"column":49},"end":{"line":16,"column":56}},"type":"binary-expr","locations":[{"start":{"line":16,"column":49},"end":{"line":16,"column":null}},{"start":{"line":16,"column":49},"end":{"line":16,"column":56}}]},"7":{"loc":{"start":{"line":12,"column":40},"end":{"line":12,"column":null}},"type":"cond-expr","locations":[{"start":{"line":12,"column":70},"end":{"line":12,"column":null}},{"start":{"line":12,"column":70},"end":{"line":12,"column":null}}]},"8":{"loc":{"start":{"line":12,"column":40},"end":{"line":12,"column":70}},"type":"binary-expr","locations":[{"start":{"line":12,"column":40},"end":{"line":12,"column":null}},{"start":{"line":12,"column":40},"end":{"line":12,"column":70}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":3,"7":3,"8":3,"9":3,"10":3,"11":2,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1},"f":{"0":3,"1":3},"b":{"0":[2,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,1],"5":[1,0],"6":[1,1],"7":[1,0],"8":[1,1]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/review-stage-notification.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/review-stage-notification.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":51}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":52}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":48}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":113}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":95}},"5":{"start":{"line":8,"column":44},"end":{"line":67,"column":null}},"6":{"start":{"line":9,"column":30},"end":{"line":9,"column":78}},"7":{"start":{"line":12,"column":25},"end":{"line":12,"column":40}},"8":{"start":{"line":17,"column":38},"end":{"line":17,"column":43}},"9":{"start":{"line":19,"column":8},"end":{"line":65,"column":9}},"10":{"start":{"line":20,"column":12},"end":{"line":57,"column":13}},"11":{"start":{"line":23,"column":24},"end":{"line":24,"column":null}},"12":{"start":{"line":26,"column":20},"end":{"line":28,"column":22}},"13":{"start":{"line":29,"column":20},"end":{"line":29,"column":27}},"14":{"start":{"line":33,"column":24},"end":{"line":33,"column":76}},"15":{"start":{"line":34,"column":20},"end":{"line":36,"column":22}},"16":{"start":{"line":37,"column":20},"end":{"line":37,"column":27}},"17":{"start":{"line":41,"column":24},"end":{"line":41,"column":78}},"18":{"start":{"line":42,"column":20},"end":{"line":44,"column":22}},"19":{"start":{"line":45,"column":20},"end":{"line":45,"column":27}},"20":{"start":{"line":49,"column":24},"end":{"line":49,"column":74}},"21":{"start":{"line":50,"column":20},"end":{"line":52,"column":22}},"22":{"start":{"line":53,"column":20},"end":{"line":53,"column":27}},"23":{"start":{"line":56,"column":20},"end":{"line":56,"column":27}},"24":{"start":{"line":60,"column":16},"end":{"line":60,"column":70}},"25":{"start":{"line":61,"column":12},"end":{"line":64,"column":14}},"26":{"start":{"line":8,"column":13},"end":{"line":8,"column":44}},"27":{"start":{"line":16,"column":10},"end":{"line":66,"column":null}},"28":{"start":{"line":8,"column":13},"end":{"line":67,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":11,"column":4},"end":{"line":11,"column":null}},"loc":{"start":{"line":12,"column":70},"end":{"line":13,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":16,"column":10},"end":{"line":16,"column":16}},"loc":{"start":{"line":16,"column":47},"end":{"line":66,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":20,"column":12},"end":{"line":57,"column":13}},"type":"switch","locations":[{"start":{"line":21,"column":16},"end":{"line":30,"column":17}},{"start":{"line":31,"column":16},"end":{"line":38,"column":17}},{"start":{"line":39,"column":16},"end":{"line":46,"column":17}},{"start":{"line":47,"column":16},"end":{"line":54,"column":17}},{"start":{"line":55,"column":16},"end":{"line":56,"column":27}}]},"1":{"loc":{"start":{"line":60,"column":16},"end":{"line":60,"column":70}},"type":"cond-expr","locations":[{"start":{"line":60,"column":41},"end":{"line":60,"column":54}},{"start":{"line":60,"column":57},"end":{"line":60,"column":70}}]},"2":{"loc":{"start":{"line":63,"column":16},"end":{"line":63,"column":64}},"type":"cond-expr","locations":[{"start":{"line":63,"column":41},"end":{"line":63,"column":52}},{"start":{"line":63,"column":55},"end":{"line":63,"column":64}}]},"3":{"loc":{"start":{"line":16,"column":24},"end":{"line":16,"column":50}},"type":"cond-expr","locations":[{"start":{"line":16,"column":47},"end":{"line":16,"column":50}},{"start":{"line":16,"column":47},"end":{"line":16,"column":50}}]},"4":{"loc":{"start":{"line":16,"column":24},"end":{"line":16,"column":47}},"type":"binary-expr","locations":[{"start":{"line":16,"column":24},"end":{"line":16,"column":50}},{"start":{"line":16,"column":24},"end":{"line":16,"column":47}}]},"5":{"loc":{"start":{"line":16,"column":50},"end":{"line":16,"column":null}},"type":"cond-expr","locations":[{"start":{"line":16,"column":57},"end":{"line":16,"column":null}},{"start":{"line":16,"column":57},"end":{"line":16,"column":null}}]},"6":{"loc":{"start":{"line":16,"column":50},"end":{"line":16,"column":57}},"type":"binary-expr","locations":[{"start":{"line":16,"column":50},"end":{"line":16,"column":null}},{"start":{"line":16,"column":50},"end":{"line":16,"column":57}}]},"7":{"loc":{"start":{"line":12,"column":40},"end":{"line":12,"column":null}},"type":"cond-expr","locations":[{"start":{"line":12,"column":70},"end":{"line":12,"column":null}},{"start":{"line":12,"column":70},"end":{"line":12,"column":null}}]},"8":{"loc":{"start":{"line":12,"column":40},"end":{"line":12,"column":70}},"type":"binary-expr","locations":[{"start":{"line":12,"column":40},"end":{"line":12,"column":null}},{"start":{"line":12,"column":40},"end":{"line":12,"column":70}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":6,"7":6,"8":6,"9":6,"10":6,"11":1,"12":1,"13":1,"14":2,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1},"f":{"0":6,"1":6},"b":{"0":[1,2,1,1,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,1],"5":[1,0],"6":[1,1],"7":[1,0],"8":[1,1]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/user-created-notification.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/listeners/user-created-notification.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":52}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":48}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":95}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":95}},"4":{"start":{"line":7,"column":44},"end":{"line":32,"column":null}},"5":{"start":{"line":8,"column":30},"end":{"line":8,"column":78}},"6":{"start":{"line":11,"column":25},"end":{"line":11,"column":40}},"7":{"start":{"line":16,"column":27},"end":{"line":16,"column":32}},"8":{"start":{"line":18,"column":8},"end":{"line":30,"column":9}},"9":{"start":{"line":19,"column":25},"end":{"line":19,"column":75}},"10":{"start":{"line":20,"column":12},"end":{"line":22,"column":14}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":70}},"12":{"start":{"line":26,"column":12},"end":{"line":29,"column":14}},"13":{"start":{"line":7,"column":13},"end":{"line":7,"column":44}},"14":{"start":{"line":15,"column":10},"end":{"line":31,"column":null}},"15":{"start":{"line":7,"column":13},"end":{"line":32,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":10,"column":4},"end":{"line":10,"column":null}},"loc":{"start":{"line":11,"column":70},"end":{"line":12,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":15,"column":10},"end":{"line":15,"column":16}},"loc":{"start":{"line":15,"column":40},"end":{"line":31,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":25,"column":16},"end":{"line":25,"column":70}},"type":"cond-expr","locations":[{"start":{"line":25,"column":41},"end":{"line":25,"column":54}},{"start":{"line":25,"column":57},"end":{"line":25,"column":70}}]},"1":{"loc":{"start":{"line":28,"column":16},"end":{"line":28,"column":64}},"type":"cond-expr","locations":[{"start":{"line":28,"column":41},"end":{"line":28,"column":52}},{"start":{"line":28,"column":55},"end":{"line":28,"column":64}}]},"2":{"loc":{"start":{"line":15,"column":24},"end":{"line":15,"column":43}},"type":"cond-expr","locations":[{"start":{"line":15,"column":40},"end":{"line":15,"column":43}},{"start":{"line":15,"column":40},"end":{"line":15,"column":43}}]},"3":{"loc":{"start":{"line":15,"column":24},"end":{"line":15,"column":40}},"type":"binary-expr","locations":[{"start":{"line":15,"column":24},"end":{"line":15,"column":43}},{"start":{"line":15,"column":24},"end":{"line":15,"column":40}}]},"4":{"loc":{"start":{"line":15,"column":43},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":50},"end":{"line":15,"column":null}},{"start":{"line":15,"column":50},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":15,"column":43},"end":{"line":15,"column":50}},"type":"binary-expr","locations":[{"start":{"line":15,"column":43},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":50}}]},"6":{"loc":{"start":{"line":11,"column":40},"end":{"line":11,"column":null}},"type":"cond-expr","locations":[{"start":{"line":11,"column":70},"end":{"line":11,"column":null}},{"start":{"line":11,"column":70},"end":{"line":11,"column":null}}]},"7":{"loc":{"start":{"line":11,"column":40},"end":{"line":11,"column":70}},"type":"binary-expr","locations":[{"start":{"line":11,"column":40},"end":{"line":11,"column":null}},{"start":{"line":11,"column":40},"end":{"line":11,"column":70}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":2,"6":2,"7":2,"8":2,"9":2,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1},"f":{"0":2,"1":2},"b":{"0":[1,0],"1":[1,0],"2":[1,0],"3":[1,1],"4":[1,0],"5":[1,1],"6":[1,0],"7":[1,1]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/ports/mailer.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/ports/mailer.port.ts","statementMap":{"0":{"start":{"line":1,"column":13},"end":{"line":1,"column":53}}},"fnMap":{},"branchMap":{},"s":{"0":5},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/ports/notification-log.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/ports/notification-log.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":6,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":4},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/services/review-email-notification.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/notifications/application/services/review-email-notification.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":60}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":47}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":null}},"3":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"4":{"start":{"line":11,"column":0},"end":{"line":11,"column":null}},"5":{"start":{"line":15,"column":0},"end":{"line":15,"column":91}},"6":{"start":{"line":16,"column":0},"end":{"line":16,"column":null}},"7":{"start":{"line":21,"column":0},"end":{"line":21,"column":null}},"8":{"start":{"line":25,"column":0},"end":{"line":25,"column":71}},"9":{"start":{"line":26,"column":0},"end":{"line":26,"column":77}},"10":{"start":{"line":27,"column":0},"end":{"line":27,"column":null}},"11":{"start":{"line":33,"column":0},"end":{"line":33,"column":58}},"12":{"start":{"line":34,"column":0},"end":{"line":34,"column":null}},"13":{"start":{"line":40,"column":43},"end":{"line":393,"column":null}},"14":{"start":{"line":41,"column":30},"end":{"line":41,"column":77}},"15":{"start":{"line":44,"column":41},"end":{"line":44,"column":49}},"16":{"start":{"line":46,"column":25},"end":{"line":46,"column":46}},"17":{"start":{"line":48,"column":25},"end":{"line":48,"column":37}},"18":{"start":{"line":50,"column":25},"end":{"line":50,"column":41}},"19":{"start":{"line":52,"column":25},"end":{"line":52,"column":39}},"20":{"start":{"line":54,"column":25},"end":{"line":54,"column":35}},"21":{"start":{"line":56,"column":25},"end":{"line":56,"column":46}},"22":{"start":{"line":57,"column":25},"end":{"line":57,"column":39}},"23":{"start":{"line":58,"column":25},"end":{"line":58,"column":40}},"24":{"start":{"line":70,"column":21},"end":{"line":70,"column":57}},"25":{"start":{"line":71,"column":8},"end":{"line":74,"column":9}},"26":{"start":{"line":72,"column":12},"end":{"line":72,"column":75}},"27":{"start":{"line":73,"column":12},"end":{"line":73,"column":21}},"28":{"start":{"line":76,"column":21},"end":{"line":86,"column":10}},"29":{"start":{"line":88,"column":8},"end":{"line":88,"column":28}},"30":{"start":{"line":97,"column":22},"end":{"line":97,"column":62}},"31":{"start":{"line":99,"column":12},"end":{"line":99,"column":65}},"32":{"start":{"line":101,"column":19},"end":{"line":101,"column":59}},"33":{"start":{"line":102,"column":8},"end":{"line":105,"column":9}},"34":{"start":{"line":103,"column":12},"end":{"line":103,"column":80}},"35":{"start":{"line":104,"column":12},"end":{"line":104,"column":21}},"36":{"start":{"line":107,"column":21},"end":{"line":120,"column":10}},"37":{"start":{"line":122,"column":8},"end":{"line":122,"column":28}},"38":{"start":{"line":126,"column":23},"end":{"line":126,"column":63}},"39":{"start":{"line":127,"column":8},"end":{"line":130,"column":9}},"40":{"start":{"line":128,"column":12},"end":{"line":128,"column":61}},"41":{"start":{"line":129,"column":12},"end":{"line":129,"column":21}},"42":{"start":{"line":132,"column":22},"end":{"line":132,"column":66}},"43":{"start":{"line":133,"column":8},"end":{"line":138,"column":9}},"44":{"start":{"line":134,"column":12},"end":{"line":136,"column":14}},"45":{"start":{"line":137,"column":12},"end":{"line":137,"column":21}},"46":{"start":{"line":140,"column":21},"end":{"line":150,"column":10}},"47":{"start":{"line":152,"column":8},"end":{"line":152,"column":28}},"48":{"start":{"line":156,"column":23},"end":{"line":156,"column":63}},"49":{"start":{"line":157,"column":8},"end":{"line":160,"column":9}},"50":{"start":{"line":158,"column":12},"end":{"line":158,"column":61}},"51":{"start":{"line":159,"column":12},"end":{"line":159,"column":21}},"52":{"start":{"line":162,"column":28},"end":{"line":164,"column":null}},"53":{"start":{"line":167,"column":24},"end":{"line":167,"column":25}},"54":{"start":{"line":169,"column":8},"end":{"line":195,"column":9}},"55":{"start":{"line":170,"column":25},"end":{"line":170,"column":78}},"56":{"start":{"line":171,"column":12},"end":{"line":176,"column":13}},"57":{"start":{"line":172,"column":16},"end":{"line":174,"column":18}},"58":{"start":{"line":175,"column":16},"end":{"line":175,"column":25}},"59":{"start":{"line":178,"column":25},"end":{"line":190,"column":14}},"60":{"start":{"line":192,"column":12},"end":{"line":194,"column":13}},"61":{"start":{"line":193,"column":16},"end":{"line":193,"column":31}},"62":{"start":{"line":197,"column":8},"end":{"line":197,"column":25}},"63":{"start":{"line":201,"column":23},"end":{"line":201,"column":63}},"64":{"start":{"line":202,"column":8},"end":{"line":205,"column":9}},"65":{"start":{"line":203,"column":12},"end":{"line":203,"column":61}},"66":{"start":{"line":204,"column":12},"end":{"line":204,"column":21}},"67":{"start":{"line":207,"column":19},"end":{"line":207,"column":60}},"68":{"start":{"line":208,"column":8},"end":{"line":213,"column":9}},"69":{"start":{"line":209,"column":12},"end":{"line":211,"column":14}},"70":{"start":{"line":212,"column":12},"end":{"line":212,"column":21}},"71":{"start":{"line":215,"column":21},"end":{"line":228,"column":10}},"72":{"start":{"line":230,"column":8},"end":{"line":230,"column":28}},"73":{"start":{"line":234,"column":23},"end":{"line":234,"column":63}},"74":{"start":{"line":235,"column":8},"end":{"line":238,"column":9}},"75":{"start":{"line":236,"column":12},"end":{"line":236,"column":61}},"76":{"start":{"line":237,"column":12},"end":{"line":237,"column":21}},"77":{"start":{"line":240,"column":26},"end":{"line":240,"column":76}},"78":{"start":{"line":242,"column":24},"end":{"line":242,"column":25}},"79":{"start":{"line":244,"column":8},"end":{"line":271,"column":9}},"80":{"start":{"line":245,"column":25},"end":{"line":245,"column":74}},"81":{"start":{"line":246,"column":12},"end":{"line":251,"column":13}},"82":{"start":{"line":247,"column":16},"end":{"line":249,"column":18}},"83":{"start":{"line":250,"column":16},"end":{"line":250,"column":25}},"84":{"start":{"line":253,"column":25},"end":{"line":266,"column":14}},"85":{"start":{"line":268,"column":12},"end":{"line":270,"column":13}},"86":{"start":{"line":269,"column":16},"end":{"line":269,"column":31}},"87":{"start":{"line":273,"column":8},"end":{"line":273,"column":25}},"88":{"start":{"line":288,"column":73},"end":{"line":288,"column":77}},"89":{"start":{"line":290,"column":8},"end":{"line":293,"column":9}},"90":{"start":{"line":291,"column":12},"end":{"line":291,"column":75}},"91":{"start":{"line":292,"column":12},"end":{"line":292,"column":25}},"92":{"start":{"line":294,"column":8},"end":{"line":297,"column":9}},"93":{"start":{"line":295,"column":12},"end":{"line":295,"column":71}},"94":{"start":{"line":296,"column":12},"end":{"line":296,"column":25}},"95":{"start":{"line":298,"column":8},"end":{"line":303,"column":9}},"96":{"start":{"line":299,"column":12},"end":{"line":301,"column":14}},"97":{"start":{"line":302,"column":12},"end":{"line":302,"column":25}},"98":{"start":{"line":305,"column":27},"end":{"line":305,"column":54}},"99":{"start":{"line":308,"column":8},"end":{"line":325,"column":9}},"100":{"start":{"line":309,"column":12},"end":{"line":313,"column":14}},"101":{"start":{"line":314,"column":15},"end":{"line":325,"column":9}},"102":{"start":{"line":315,"column":12},"end":{"line":319,"column":14}},"103":{"start":{"line":321,"column":12},"end":{"line":324,"column":14}},"104":{"start":{"line":327,"column":8},"end":{"line":332,"column":9}},"105":{"start":{"line":328,"column":12},"end":{"line":330,"column":14}},"106":{"start":{"line":331,"column":12},"end":{"line":331,"column":25}},"107":{"start":{"line":335,"column":12},"end":{"line":344,"column":14}},"108":{"start":{"line":346,"column":24},"end":{"line":353,"column":10}},"109":{"start":{"line":355,"column":8},"end":{"line":384,"column":9}},"110":{"start":{"line":356,"column":12},"end":{"line":369,"column":15}},"111":{"start":{"line":370,"column":12},"end":{"line":370,"column":61}},"112":{"start":{"line":371,"column":12},"end":{"line":373,"column":14}},"113":{"start":{"line":374,"column":12},"end":{"line":374,"column":24}},"114":{"start":{"line":377,"column":16},"end":{"line":377,"column":70}},"115":{"start":{"line":378,"column":12},"end":{"line":381,"column":14}},"116":{"start":{"line":382,"column":12},"end":{"line":382,"column":73}},"117":{"start":{"line":383,"column":12},"end":{"line":383,"column":25}},"118":{"start":{"line":388,"column":21},"end":{"line":388,"column":78}},"119":{"start":{"line":389,"column":28},"end":{"line":389,"column":73}},"120":{"start":{"line":390,"column":28},"end":{"line":390,"column":71}},"121":{"start":{"line":391,"column":8},"end":{"line":391,"column":47}},"122":{"start":{"line":40,"column":13},"end":{"line":40,"column":43}},"123":{"start":{"line":40,"column":13},"end":{"line":393,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":43,"column":4},"end":{"line":43,"column":null}},"loc":{"start":{"line":58,"column":53},"end":{"line":59,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":69,"column":4},"end":{"line":69,"column":9}},"loc":{"start":{"line":69,"column":42},"end":{"line":89,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":96,"column":4},"end":{"line":96,"column":9}},"loc":{"start":{"line":96,"column":57},"end":{"line":123,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":125,"column":4},"end":{"line":125,"column":9}},"loc":{"start":{"line":125,"column":52},"end":{"line":153,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":155,"column":4},"end":{"line":155,"column":9}},"loc":{"start":{"line":155,"column":44},"end":{"line":198,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":200,"column":4},"end":{"line":200,"column":9}},"loc":{"start":{"line":200,"column":46},"end":{"line":231,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":233,"column":4},"end":{"line":233,"column":9}},"loc":{"start":{"line":233,"column":42},"end":{"line":274,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":276,"column":12},"end":{"line":276,"column":17}},"loc":{"start":{"line":287,"column":5},"end":{"line":385,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":387,"column":12},"end":{"line":387,"column":20}},"loc":{"start":{"line":387,"column":33},"end":{"line":392,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":71,"column":8},"end":{"line":74,"column":9}},"type":"if","locations":[{"start":{"line":71,"column":8},"end":{"line":74,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":88,"column":15},"end":{"line":88,"column":27}},"type":"cond-expr","locations":[{"start":{"line":88,"column":22},"end":{"line":88,"column":23}},{"start":{"line":88,"column":26},"end":{"line":88,"column":27}}]},"2":{"loc":{"start":{"line":102,"column":8},"end":{"line":105,"column":9}},"type":"if","locations":[{"start":{"line":102,"column":8},"end":{"line":105,"column":9}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":122,"column":15},"end":{"line":122,"column":27}},"type":"cond-expr","locations":[{"start":{"line":122,"column":22},"end":{"line":122,"column":23}},{"start":{"line":122,"column":26},"end":{"line":122,"column":27}}]},"4":{"loc":{"start":{"line":127,"column":8},"end":{"line":130,"column":9}},"type":"if","locations":[{"start":{"line":127,"column":8},"end":{"line":130,"column":9}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":133,"column":8},"end":{"line":138,"column":9}},"type":"if","locations":[{"start":{"line":133,"column":8},"end":{"line":138,"column":9}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":152,"column":15},"end":{"line":152,"column":27}},"type":"cond-expr","locations":[{"start":{"line":152,"column":22},"end":{"line":152,"column":23}},{"start":{"line":152,"column":26},"end":{"line":152,"column":27}}]},"7":{"loc":{"start":{"line":157,"column":8},"end":{"line":160,"column":9}},"type":"if","locations":[{"start":{"line":157,"column":8},"end":{"line":160,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":171,"column":12},"end":{"line":176,"column":13}},"type":"if","locations":[{"start":{"line":171,"column":12},"end":{"line":176,"column":13}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":192,"column":12},"end":{"line":194,"column":13}},"type":"if","locations":[{"start":{"line":192,"column":12},"end":{"line":194,"column":13}},{"start":{},"end":{}}]},"10":{"loc":{"start":{"line":202,"column":8},"end":{"line":205,"column":9}},"type":"if","locations":[{"start":{"line":202,"column":8},"end":{"line":205,"column":9}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":208,"column":8},"end":{"line":213,"column":9}},"type":"if","locations":[{"start":{"line":208,"column":8},"end":{"line":213,"column":9}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":230,"column":15},"end":{"line":230,"column":27}},"type":"cond-expr","locations":[{"start":{"line":230,"column":22},"end":{"line":230,"column":23}},{"start":{"line":230,"column":26},"end":{"line":230,"column":27}}]},"13":{"loc":{"start":{"line":235,"column":8},"end":{"line":238,"column":9}},"type":"if","locations":[{"start":{"line":235,"column":8},"end":{"line":238,"column":9}},{"start":{},"end":{}}]},"14":{"loc":{"start":{"line":246,"column":12},"end":{"line":251,"column":13}},"type":"if","locations":[{"start":{"line":246,"column":12},"end":{"line":251,"column":13}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":268,"column":12},"end":{"line":270,"column":13}},"type":"if","locations":[{"start":{"line":268,"column":12},"end":{"line":270,"column":13}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":290,"column":8},"end":{"line":293,"column":9}},"type":"if","locations":[{"start":{"line":290,"column":8},"end":{"line":293,"column":9}},{"start":{},"end":{}}]},"17":{"loc":{"start":{"line":294,"column":8},"end":{"line":297,"column":9}},"type":"if","locations":[{"start":{"line":294,"column":8},"end":{"line":297,"column":9}},{"start":{},"end":{}}]},"18":{"loc":{"start":{"line":298,"column":8},"end":{"line":303,"column":9}},"type":"if","locations":[{"start":{"line":298,"column":8},"end":{"line":303,"column":9}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":308,"column":8},"end":{"line":325,"column":9}},"type":"if","locations":[{"start":{"line":308,"column":8},"end":{"line":325,"column":9}},{"start":{"line":314,"column":15},"end":{"line":325,"column":9}}]},"20":{"loc":{"start":{"line":314,"column":15},"end":{"line":325,"column":9}},"type":"if","locations":[{"start":{"line":314,"column":15},"end":{"line":325,"column":9}},{"start":{"line":320,"column":15},"end":{"line":325,"column":9}}]},"21":{"loc":{"start":{"line":327,"column":8},"end":{"line":332,"column":9}},"type":"if","locations":[{"start":{"line":327,"column":8},"end":{"line":332,"column":9}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":335,"column":12},"end":{"line":344,"column":14}},"type":"binary-expr","locations":[{"start":{"line":335,"column":12},"end":{"line":335,"column":20}},{"start":{"line":336,"column":13},"end":{"line":343,"column":null}}]},"23":{"loc":{"start":{"line":338,"column":30},"end":{"line":338,"column":71}},"type":"cond-expr","locations":[{"start":{"line":338,"column":56},"end":{"line":338,"column":64}},{"start":{"line":338,"column":67},"end":{"line":338,"column":71}}]},"24":{"loc":{"start":{"line":339,"column":29},"end":{"line":339,"column":69}},"type":"cond-expr","locations":[{"start":{"line":339,"column":54},"end":{"line":339,"column":62}},{"start":{"line":339,"column":65},"end":{"line":339,"column":69}}]},"25":{"loc":{"start":{"line":377,"column":16},"end":{"line":377,"column":70}},"type":"cond-expr","locations":[{"start":{"line":377,"column":41},"end":{"line":377,"column":54}},{"start":{"line":377,"column":57},"end":{"line":377,"column":70}}]},"26":{"loc":{"start":{"line":380,"column":16},"end":{"line":380,"column":64}},"type":"cond-expr","locations":[{"start":{"line":380,"column":41},"end":{"line":380,"column":52}},{"start":{"line":380,"column":55},"end":{"line":380,"column":64}}]},"27":{"loc":{"start":{"line":388,"column":21},"end":{"line":388,"column":78}},"type":"binary-expr","locations":[{"start":{"line":388,"column":21},"end":{"line":388,"column":71}},{"start":{"line":388,"column":75},"end":{"line":388,"column":78}}]},"28":{"loc":{"start":{"line":389,"column":28},"end":{"line":389,"column":73}},"type":"cond-expr","locations":[{"start":{"line":389,"column":49},"end":{"line":389,"column":66}},{"start":{"line":389,"column":69},"end":{"line":389,"column":73}}]},"29":{"loc":{"start":{"line":390,"column":28},"end":{"line":390,"column":71}},"type":"cond-expr","locations":[{"start":{"line":390,"column":51},"end":{"line":390,"column":64}},{"start":{"line":390,"column":67},"end":{"line":390,"column":71}}]},"30":{"loc":{"start":{"line":44,"column":49},"end":{"line":44,"column":null}},"type":"cond-expr","locations":[{"start":{"line":44,"column":59},"end":{"line":44,"column":null}},{"start":{"line":44,"column":59},"end":{"line":44,"column":null}}]},"31":{"loc":{"start":{"line":44,"column":49},"end":{"line":44,"column":59}},"type":"binary-expr","locations":[{"start":{"line":44,"column":49},"end":{"line":44,"column":null}},{"start":{"line":44,"column":49},"end":{"line":44,"column":59}}]},"32":{"loc":{"start":{"line":44,"column":59},"end":{"line":46,"column":null}},"type":"cond-expr","locations":[{"start":{"line":46,"column":75},"end":{"line":46,"column":null}},{"start":{"line":46,"column":75},"end":{"line":46,"column":null}}]},"33":{"loc":{"start":{"line":44,"column":59},"end":{"line":46,"column":75}},"type":"binary-expr","locations":[{"start":{"line":44,"column":59},"end":{"line":46,"column":null}},{"start":{"line":46,"column":46},"end":{"line":46,"column":75}}]},"34":{"loc":{"start":{"line":46,"column":75},"end":{"line":48,"column":null}},"type":"cond-expr","locations":[{"start":{"line":48,"column":57},"end":{"line":48,"column":null}},{"start":{"line":48,"column":57},"end":{"line":48,"column":null}}]},"35":{"loc":{"start":{"line":46,"column":75},"end":{"line":48,"column":57}},"type":"binary-expr","locations":[{"start":{"line":46,"column":75},"end":{"line":48,"column":null}},{"start":{"line":48,"column":37},"end":{"line":48,"column":57}}]},"36":{"loc":{"start":{"line":48,"column":57},"end":{"line":50,"column":null}},"type":"cond-expr","locations":[{"start":{"line":50,"column":65},"end":{"line":50,"column":null}},{"start":{"line":50,"column":65},"end":{"line":50,"column":null}}]},"37":{"loc":{"start":{"line":48,"column":57},"end":{"line":50,"column":65}},"type":"binary-expr","locations":[{"start":{"line":48,"column":57},"end":{"line":50,"column":null}},{"start":{"line":50,"column":41},"end":{"line":50,"column":65}}]},"38":{"loc":{"start":{"line":50,"column":65},"end":{"line":52,"column":null}},"type":"cond-expr","locations":[{"start":{"line":52,"column":61},"end":{"line":52,"column":null}},{"start":{"line":52,"column":61},"end":{"line":52,"column":null}}]},"39":{"loc":{"start":{"line":50,"column":65},"end":{"line":52,"column":61}},"type":"binary-expr","locations":[{"start":{"line":50,"column":65},"end":{"line":52,"column":null}},{"start":{"line":52,"column":39},"end":{"line":52,"column":61}}]},"40":{"loc":{"start":{"line":52,"column":61},"end":{"line":54,"column":null}},"type":"cond-expr","locations":[{"start":{"line":54,"column":53},"end":{"line":54,"column":null}},{"start":{"line":54,"column":53},"end":{"line":54,"column":null}}]},"41":{"loc":{"start":{"line":52,"column":61},"end":{"line":54,"column":53}},"type":"binary-expr","locations":[{"start":{"line":52,"column":61},"end":{"line":54,"column":null}},{"start":{"line":54,"column":35},"end":{"line":54,"column":53}}]},"42":{"loc":{"start":{"line":54,"column":53},"end":{"line":56,"column":null}},"type":"cond-expr","locations":[{"start":{"line":56,"column":75},"end":{"line":56,"column":null}},{"start":{"line":56,"column":75},"end":{"line":56,"column":null}}]},"43":{"loc":{"start":{"line":54,"column":53},"end":{"line":56,"column":75}},"type":"binary-expr","locations":[{"start":{"line":54,"column":53},"end":{"line":56,"column":null}},{"start":{"line":56,"column":46},"end":{"line":56,"column":75}}]},"44":{"loc":{"start":{"line":56,"column":75},"end":{"line":57,"column":null}},"type":"cond-expr","locations":[{"start":{"line":57,"column":51},"end":{"line":57,"column":null}},{"start":{"line":57,"column":51},"end":{"line":57,"column":null}}]},"45":{"loc":{"start":{"line":56,"column":75},"end":{"line":57,"column":51}},"type":"binary-expr","locations":[{"start":{"line":56,"column":75},"end":{"line":57,"column":null}},{"start":{"line":57,"column":39},"end":{"line":57,"column":51}}]},"46":{"loc":{"start":{"line":57,"column":51},"end":{"line":58,"column":null}},"type":"cond-expr","locations":[{"start":{"line":58,"column":53},"end":{"line":58,"column":null}},{"start":{"line":58,"column":53},"end":{"line":58,"column":null}}]},"47":{"loc":{"start":{"line":57,"column":51},"end":{"line":58,"column":53}},"type":"binary-expr","locations":[{"start":{"line":57,"column":51},"end":{"line":58,"column":null}},{"start":{"line":58,"column":40},"end":{"line":58,"column":53}}]}},"s":{"0":4,"1":4,"2":4,"3":4,"4":4,"5":4,"6":4,"7":4,"8":4,"9":4,"10":4,"11":4,"12":4,"13":4,"14":19,"15":19,"16":19,"17":19,"18":19,"19":19,"20":19,"21":19,"22":19,"23":19,"24":8,"25":8,"26":1,"27":1,"28":7,"29":7,"30":2,"31":2,"32":2,"33":2,"34":1,"35":1,"36":1,"37":1,"38":3,"39":3,"40":1,"41":1,"42":2,"43":2,"44":1,"45":1,"46":1,"47":1,"48":2,"49":2,"50":1,"51":1,"52":1,"53":1,"54":1,"55":2,"56":2,"57":1,"58":1,"59":1,"60":1,"61":1,"62":1,"63":3,"64":3,"65":1,"66":1,"67":2,"68":2,"69":1,"70":1,"71":1,"72":1,"73":2,"74":2,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":0,"83":0,"84":1,"85":1,"86":1,"87":1,"88":12,"89":12,"90":0,"91":0,"92":12,"93":0,"94":0,"95":12,"96":1,"97":1,"98":11,"99":11,"100":4,"101":7,"102":1,"103":6,"104":11,"105":1,"106":1,"107":10,"108":10,"109":10,"110":10,"111":9,"112":9,"113":9,"114":1,"115":1,"116":1,"117":1,"118":12,"119":12,"120":12,"121":12,"122":4,"123":4},"f":{"0":19,"1":8,"2":2,"3":3,"4":2,"5":3,"6":2,"7":12,"8":12},"b":{"0":[1,7],"1":[4,3],"2":[1,1],"3":[1,0],"4":[1,2],"5":[1,1],"6":[1,0],"7":[1,1],"8":[1,1],"9":[1,0],"10":[1,2],"11":[1,1],"12":[1,0],"13":[1,1],"14":[0,1],"15":[1,0],"16":[0,12],"17":[0,12],"18":[1,11],"19":[4,7],"20":[1,6],"21":[1,10],"22":[10,9],"23":[4,5],"24":[1,8],"25":[1,0],"26":[1,0],"27":[12,1],"28":[12,0],"29":[0,12],"30":[0,4],"31":[4,0],"32":[0,4],"33":[4,0],"34":[0,4],"35":[4,0],"36":[0,4],"37":[4,0],"38":[0,4],"39":[4,0],"40":[0,4],"41":[4,0],"42":[0,4],"43":[4,0],"44":[4,0],"45":[4,4],"46":[4,0],"47":[4,4]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/ports/position-hierarchy.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/ports/position-hierarchy.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":2},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/ports/position.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/ports/position.repository.port.ts","statementMap":{"0":{"start":{"line":7,"column":13},"end":{"line":9,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":24},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/ports/team.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/ports/team.repository.port.ts","statementMap":{"0":{"start":{"line":5,"column":13},"end":{"line":7,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":7},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/position-hierarchy.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/position-hierarchy.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":73}},"1":{"start":{"line":5,"column":0},"end":{"line":5,"column":105}},"2":{"start":{"line":6,"column":0},"end":{"line":6,"column":53}},"3":{"start":{"line":9,"column":7},"end":{"line":67,"column":null}},"4":{"start":{"line":12,"column":25},"end":{"line":12,"column":67}},"5":{"start":{"line":13,"column":25},"end":{"line":13,"column":36}},"6":{"start":{"line":20,"column":8},"end":{"line":24,"column":9}},"7":{"start":{"line":21,"column":12},"end":{"line":23,"column":14}},"8":{"start":{"line":26,"column":8},"end":{"line":26,"column":57}},"9":{"start":{"line":27,"column":8},"end":{"line":27,"column":60}},"10":{"start":{"line":29,"column":8},"end":{"line":29,"column":78}},"11":{"start":{"line":36,"column":8},"end":{"line":36,"column":57}},"12":{"start":{"line":37,"column":8},"end":{"line":37,"column":60}},"13":{"start":{"line":38,"column":8},"end":{"line":38,"column":79}},"14":{"start":{"line":44,"column":8},"end":{"line":44,"column":57}},"15":{"start":{"line":46,"column":12},"end":{"line":46,"column":69}},"16":{"start":{"line":47,"column":8},"end":{"line":51,"column":10}},"17":{"start":{"line":49,"column":16},"end":{"line":49,"column":65}},"18":{"start":{"line":57,"column":8},"end":{"line":57,"column":60}},"19":{"start":{"line":58,"column":26},"end":{"line":59,"column":null}},"20":{"start":{"line":61,"column":8},"end":{"line":65,"column":10}},"21":{"start":{"line":63,"column":16},"end":{"line":63,"column":62}},"22":{"start":{"line":9,"column":13},"end":{"line":9,"column":37}},"23":{"start":{"line":9,"column":13},"end":{"line":67,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":10,"column":4},"end":{"line":10,"column":null}},"loc":{"start":{"line":13,"column":51},"end":{"line":14,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":16,"column":4},"end":{"line":16,"column":9}},"loc":{"start":{"line":18,"column":37},"end":{"line":30,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":32,"column":4},"end":{"line":32,"column":9}},"loc":{"start":{"line":34,"column":37},"end":{"line":39,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":41,"column":4},"end":{"line":41,"column":9}},"loc":{"start":{"line":42,"column":34},"end":{"line":52,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":48,"column":26},"end":{"line":48,"column":27}},"loc":{"start":{"line":49,"column":16},"end":{"line":49,"column":65}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":54,"column":4},"end":{"line":54,"column":9}},"loc":{"start":{"line":55,"column":37},"end":{"line":66,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":62,"column":26},"end":{"line":62,"column":27}},"loc":{"start":{"line":63,"column":16},"end":{"line":63,"column":62}}}},"branchMap":{"0":{"loc":{"start":{"line":20,"column":8},"end":{"line":24,"column":9}},"type":"if","locations":[{"start":{"line":20,"column":8},"end":{"line":24,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":13,"column":36},"end":{"line":13,"column":null}},"type":"cond-expr","locations":[{"start":{"line":13,"column":51},"end":{"line":13,"column":null}},{"start":{"line":13,"column":51},"end":{"line":13,"column":null}}]},"2":{"loc":{"start":{"line":13,"column":36},"end":{"line":13,"column":51}},"type":"binary-expr","locations":[{"start":{"line":13,"column":36},"end":{"line":13,"column":null}},{"start":{"line":13,"column":36},"end":{"line":13,"column":51}}]}},"s":{"0":2,"1":2,"2":2,"3":2,"4":5,"5":5,"6":2,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":2,"18":1,"19":1,"20":1,"21":2,"22":2,"23":2},"f":{"0":5,"1":2,"2":1,"3":1,"4":2,"5":1,"6":2},"b":{"0":[1,1],"1":[2,0],"2":[2,2]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/position.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/position.service.ts","statementMap":{"0":{"start":{"line":6,"column":0},"end":{"line":6,"column":null}},"1":{"start":{"line":12,"column":0},"end":{"line":12,"column":null}},"2":{"start":{"line":16,"column":0},"end":{"line":16,"column":62}},"3":{"start":{"line":18,"column":0},"end":{"line":18,"column":85}},"4":{"start":{"line":21,"column":7},"end":{"line":84,"column":null}},"5":{"start":{"line":24,"column":25},"end":{"line":24,"column":58}},"6":{"start":{"line":26,"column":25},"end":{"line":26,"column":54}},"7":{"start":{"line":30,"column":25},"end":{"line":33,"column":10}},"8":{"start":{"line":34,"column":8},"end":{"line":34,"column":47}},"9":{"start":{"line":38,"column":8},"end":{"line":38,"column":44}},"10":{"start":{"line":42,"column":22},"end":{"line":42,"column":55}},"11":{"start":{"line":43,"column":8},"end":{"line":43,"column":70}},"12":{"start":{"line":43,"column":20},"end":{"line":43,"column":70}},"13":{"start":{"line":44,"column":8},"end":{"line":44,"column":21}},"14":{"start":{"line":51,"column":8},"end":{"line":51,"column":31}},"15":{"start":{"line":52,"column":47},"end":{"line":57,"column":10}},"16":{"start":{"line":58,"column":8},"end":{"line":58,"column":54}},"17":{"start":{"line":62,"column":8},"end":{"line":62,"column":31}},"18":{"start":{"line":63,"column":28},"end":{"line":63,"column":58}},"19":{"start":{"line":64,"column":8},"end":{"line":72,"column":9}},"20":{"start":{"line":65,"column":12},"end":{"line":71,"column":14}},"21":{"start":{"line":75,"column":8},"end":{"line":75,"column":44}},"22":{"start":{"line":79,"column":8},"end":{"line":79,"column":39}},"23":{"start":{"line":81,"column":12},"end":{"line":81,"column":77}},"24":{"start":{"line":82,"column":8},"end":{"line":82,"column":52}},"25":{"start":{"line":82,"column":36},"end":{"line":82,"column":50}},"26":{"start":{"line":21,"column":13},"end":{"line":21,"column":28}},"27":{"start":{"line":21,"column":13},"end":{"line":84,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":22,"column":4},"end":{"line":22,"column":null}},"loc":{"start":{"line":26,"column":94},"end":{"line":27,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":29,"column":4},"end":{"line":29,"column":9}},"loc":{"start":{"line":29,"column":47},"end":{"line":35,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":37,"column":4},"end":{"line":37,"column":9}},"loc":{"start":{"line":37,"column":43},"end":{"line":39,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":41,"column":4},"end":{"line":41,"column":9}},"loc":{"start":{"line":41,"column":28},"end":{"line":45,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":47,"column":4},"end":{"line":47,"column":9}},"loc":{"start":{"line":49,"column":36},"end":{"line":59,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":61,"column":4},"end":{"line":61,"column":9}},"loc":{"start":{"line":61,"column":27},"end":{"line":76,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":78,"column":4},"end":{"line":78,"column":9}},"loc":{"start":{"line":78,"column":44},"end":{"line":83,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":82,"column":29},"end":{"line":82,"column":30}},"loc":{"start":{"line":82,"column":36},"end":{"line":82,"column":50}}}},"branchMap":{"0":{"loc":{"start":{"line":32,"column":25},"end":{"line":32,"column":52}},"type":"binary-expr","locations":[{"start":{"line":32,"column":25},"end":{"line":32,"column":44}},{"start":{"line":32,"column":48},"end":{"line":32,"column":52}}]},"1":{"loc":{"start":{"line":43,"column":8},"end":{"line":43,"column":70}},"type":"if","locations":[{"start":{"line":43,"column":8},"end":{"line":43,"column":70}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":54,"column":16},"end":{"line":56,"column":20}},"type":"cond-expr","locations":[{"start":{"line":55,"column":18},"end":{"line":55,"column":52}},{"start":{"line":56,"column":18},"end":{"line":56,"column":20}}]},"3":{"loc":{"start":{"line":64,"column":8},"end":{"line":72,"column":9}},"type":"if","locations":[{"start":{"line":64,"column":8},"end":{"line":72,"column":9}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":26,"column":54},"end":{"line":26,"column":null}},"type":"cond-expr","locations":[{"start":{"line":26,"column":94},"end":{"line":26,"column":null}},{"start":{"line":26,"column":94},"end":{"line":26,"column":null}}]},"5":{"loc":{"start":{"line":26,"column":54},"end":{"line":26,"column":94}},"type":"binary-expr","locations":[{"start":{"line":26,"column":54},"end":{"line":26,"column":null}},{"start":{"line":26,"column":54},"end":{"line":26,"column":94}}]}},"s":{"0":19,"1":19,"2":19,"3":19,"4":19,"5":11,"6":11,"7":2,"8":2,"9":1,"10":10,"11":10,"12":3,"13":7,"14":2,"15":1,"16":1,"17":3,"18":2,"19":2,"20":1,"21":1,"22":3,"23":3,"24":3,"25":3,"26":19,"27":19},"f":{"0":11,"1":2,"2":1,"3":10,"4":2,"5":3,"6":3,"7":3},"b":{"0":[2,1],"1":[3,7],"2":[1,0],"3":[1,1],"4":[0,19],"5":[19,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/team.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/organisation/application/services/team.service.ts","statementMap":{"0":{"start":{"line":7,"column":0},"end":{"line":7,"column":null}},"1":{"start":{"line":13,"column":0},"end":{"line":13,"column":103}},"2":{"start":{"line":15,"column":0},"end":{"line":15,"column":54}},"3":{"start":{"line":17,"column":0},"end":{"line":17,"column":77}},"4":{"start":{"line":20,"column":7},"end":{"line":130,"column":null}},"5":{"start":{"line":23,"column":25},"end":{"line":23,"column":50}},"6":{"start":{"line":24,"column":25},"end":{"line":24,"column":40}},"7":{"start":{"line":28,"column":8},"end":{"line":30,"column":9}},"8":{"start":{"line":29,"column":12},"end":{"line":29,"column":61}},"9":{"start":{"line":32,"column":21},"end":{"line":36,"column":10}},"10":{"start":{"line":37,"column":8},"end":{"line":37,"column":39}},"11":{"start":{"line":41,"column":8},"end":{"line":41,"column":40}},"12":{"start":{"line":45,"column":21},"end":{"line":45,"column":50}},"13":{"start":{"line":46,"column":8},"end":{"line":46,"column":65}},"14":{"start":{"line":46,"column":19},"end":{"line":46,"column":65}},"15":{"start":{"line":47,"column":8},"end":{"line":47,"column":20}},"16":{"start":{"line":51,"column":8},"end":{"line":51,"column":31}},"17":{"start":{"line":53,"column":8},"end":{"line":55,"column":9}},"18":{"start":{"line":54,"column":12},"end":{"line":54,"column":59}},"19":{"start":{"line":57,"column":43},"end":{"line":62,"column":10}},"20":{"start":{"line":64,"column":8},"end":{"line":64,"column":50}},"21":{"start":{"line":68,"column":8},"end":{"line":68,"column":31}},"22":{"start":{"line":69,"column":24},"end":{"line":69,"column":50}},"23":{"start":{"line":70,"column":8},"end":{"line":78,"column":9}},"24":{"start":{"line":71,"column":12},"end":{"line":77,"column":14}},"25":{"start":{"line":79,"column":8},"end":{"line":79,"column":40}},"26":{"start":{"line":87,"column":8},"end":{"line":87,"column":35}},"27":{"start":{"line":88,"column":21},"end":{"line":88,"column":71}},"28":{"start":{"line":90,"column":27},"end":{"line":93,"column":null}},"29":{"start":{"line":95,"column":8},"end":{"line":97,"column":9}},"30":{"start":{"line":96,"column":12},"end":{"line":96,"column":45}},"31":{"start":{"line":98,"column":8},"end":{"line":98,"column":26}},"32":{"start":{"line":102,"column":8},"end":{"line":102,"column":35}},"33":{"start":{"line":103,"column":8},"end":{"line":103,"column":56}},"34":{"start":{"line":110,"column":8},"end":{"line":110,"column":35}},"35":{"start":{"line":111,"column":28},"end":{"line":111,"column":64}},"36":{"start":{"line":113,"column":8},"end":{"line":113,"column":49}},"37":{"start":{"line":113,"column":30},"end":{"line":113,"column":49}},"38":{"start":{"line":115,"column":26},"end":{"line":125,"column":null}},"39":{"start":{"line":117,"column":16},"end":{"line":124,"column":17}},"40":{"start":{"line":118,"column":33},"end":{"line":119,"column":null}},"41":{"start":{"line":121,"column":20},"end":{"line":121,"column":53}},"42":{"start":{"line":123,"column":20},"end":{"line":123,"column":38}},"43":{"start":{"line":128,"column":8},"end":{"line":128,"column":25}},"44":{"start":{"line":20,"column":13},"end":{"line":20,"column":24}},"45":{"start":{"line":20,"column":13},"end":{"line":130,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"loc":{"start":{"line":24,"column":59},"end":{"line":25,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":27,"column":4},"end":{"line":27,"column":9}},"loc":{"start":{"line":27,"column":43},"end":{"line":38,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":40,"column":4},"end":{"line":40,"column":9}},"loc":{"start":{"line":40,"column":39},"end":{"line":42,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":44,"column":4},"end":{"line":44,"column":9}},"loc":{"start":{"line":44,"column":28},"end":{"line":48,"column":5}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":50,"column":4},"end":{"line":50,"column":9}},"loc":{"start":{"line":50,"column":53},"end":{"line":65,"column":5}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":67,"column":4},"end":{"line":67,"column":9}},"loc":{"start":{"line":67,"column":27},"end":{"line":80,"column":5}}},"6":{"name":"(anonymous_10)","decl":{"start":{"line":82,"column":4},"end":{"line":82,"column":9}},"loc":{"start":{"line":85,"column":37},"end":{"line":99,"column":5}}},"7":{"name":"(anonymous_11)","decl":{"start":{"line":101,"column":4},"end":{"line":101,"column":9}},"loc":{"start":{"line":101,"column":55},"end":{"line":104,"column":5}}},"8":{"name":"(anonymous_12)","decl":{"start":{"line":106,"column":4},"end":{"line":106,"column":9}},"loc":{"start":{"line":108,"column":38},"end":{"line":129,"column":5}}},"9":{"name":"(anonymous_13)","decl":{"start":{"line":116,"column":28},"end":{"line":116,"column":33}},"loc":{"start":{"line":116,"column":49},"end":{"line":125,"column":13}}}},"branchMap":{"0":{"loc":{"start":{"line":28,"column":8},"end":{"line":30,"column":9}},"type":"if","locations":[{"start":{"line":28,"column":8},"end":{"line":30,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":28,"column":12},"end":{"line":28,"column":67}},"type":"binary-expr","locations":[{"start":{"line":28,"column":12},"end":{"line":28,"column":40}},{"start":{"line":28,"column":44},"end":{"line":28,"column":67}}]},"2":{"loc":{"start":{"line":34,"column":25},"end":{"line":34,"column":52}},"type":"binary-expr","locations":[{"start":{"line":34,"column":25},"end":{"line":34,"column":44}},{"start":{"line":34,"column":48},"end":{"line":34,"column":52}}]},"3":{"loc":{"start":{"line":35,"column":20},"end":{"line":35,"column":42}},"type":"binary-expr","locations":[{"start":{"line":35,"column":20},"end":{"line":35,"column":34}},{"start":{"line":35,"column":38},"end":{"line":35,"column":42}}]},"4":{"loc":{"start":{"line":46,"column":8},"end":{"line":46,"column":65}},"type":"if","locations":[{"start":{"line":46,"column":8},"end":{"line":46,"column":65}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":53,"column":8},"end":{"line":55,"column":9}},"type":"if","locations":[{"start":{"line":53,"column":8},"end":{"line":55,"column":9}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":53,"column":12},"end":{"line":53,"column":63}},"type":"binary-expr","locations":[{"start":{"line":53,"column":12},"end":{"line":53,"column":38}},{"start":{"line":53,"column":42},"end":{"line":53,"column":63}}]},"7":{"loc":{"start":{"line":59,"column":16},"end":{"line":61,"column":20}},"type":"cond-expr","locations":[{"start":{"line":60,"column":18},"end":{"line":60,"column":52}},{"start":{"line":61,"column":18},"end":{"line":61,"column":20}}]},"8":{"loc":{"start":{"line":70,"column":8},"end":{"line":78,"column":9}},"type":"if","locations":[{"start":{"line":70,"column":8},"end":{"line":78,"column":9}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":95,"column":8},"end":{"line":97,"column":9}},"type":"if","locations":[{"start":{"line":95,"column":8},"end":{"line":97,"column":9}},{"start":{},"end":{}}]},"10":{"loc":{"start":{"line":113,"column":8},"end":{"line":113,"column":49}},"type":"if","locations":[{"start":{"line":113,"column":8},"end":{"line":113,"column":49}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":24,"column":40},"end":{"line":24,"column":null}},"type":"cond-expr","locations":[{"start":{"line":24,"column":59},"end":{"line":24,"column":null}},{"start":{"line":24,"column":59},"end":{"line":24,"column":null}}]},"12":{"loc":{"start":{"line":24,"column":40},"end":{"line":24,"column":59}},"type":"binary-expr","locations":[{"start":{"line":24,"column":40},"end":{"line":24,"column":null}},{"start":{"line":24,"column":40},"end":{"line":24,"column":59}}]}},"s":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":19,"6":19,"7":3,"8":1,"9":3,"10":3,"11":1,"12":17,"13":17,"14":3,"15":14,"16":4,"17":3,"18":1,"19":3,"20":3,"21":3,"22":2,"23":2,"24":1,"25":1,"26":2,"27":2,"28":2,"29":2,"30":1,"31":1,"32":1,"33":1,"34":5,"35":5,"36":5,"37":3,"38":2,"39":3,"40":3,"41":2,"42":1,"43":2,"44":2,"45":2},"f":{"0":19,"1":3,"2":1,"3":17,"4":4,"5":3,"6":2,"7":1,"8":5,"9":3},"b":{"0":[1,2],"1":[3,2],"2":[3,3],"3":[3,2],"4":[3,14],"5":[1,2],"6":[3,2],"7":[1,2],"8":[1,1],"9":[1,1],"10":[3,2],"11":[2,0],"12":[2,2]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/listeners/cycle-stage.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/listeners/cycle-stage.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":50}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":60}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":48}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":114}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":111}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":null}},"6":{"start":{"line":10,"column":0},"end":{"line":10,"column":87}},"7":{"start":{"line":18,"column":31},"end":{"line":86,"column":null}},"8":{"start":{"line":19,"column":30},"end":{"line":19,"column":65}},"9":{"start":{"line":22,"column":25},"end":{"line":22,"column":52}},"10":{"start":{"line":23,"column":25},"end":{"line":23,"column":39}},"11":{"start":{"line":25,"column":25},"end":{"line":25,"column":34}},"12":{"start":{"line":39,"column":8},"end":{"line":41,"column":9}},"13":{"start":{"line":40,"column":12},"end":{"line":40,"column":19}},"14":{"start":{"line":43,"column":8},"end":{"line":45,"column":10}},"15":{"start":{"line":47,"column":8},"end":{"line":54,"column":18}},"16":{"start":{"line":48,"column":28},"end":{"line":48,"column":75}},"17":{"start":{"line":49,"column":12},"end":{"line":53,"column":13}},"18":{"start":{"line":50,"column":16},"end":{"line":52,"column":18}},"19":{"start":{"line":56,"column":8},"end":{"line":58,"column":10}},"20":{"start":{"line":60,"column":8},"end":{"line":84,"column":9}},"21":{"start":{"line":61,"column":12},"end":{"line":64,"column":14}},"22":{"start":{"line":67,"column":16},"end":{"line":68,"column":null}},"23":{"start":{"line":71,"column":12},"end":{"line":73,"column":14}},"24":{"start":{"line":75,"column":12},"end":{"line":78,"column":14}},"25":{"start":{"line":80,"column":12},"end":{"line":83,"column":14}},"26":{"start":{"line":18,"column":13},"end":{"line":18,"column":31}},"27":{"start":{"line":36,"column":10},"end":{"line":85,"column":null}},"28":{"start":{"line":18,"column":13},"end":{"line":86,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":21,"column":4},"end":{"line":21,"column":null}},"loc":{"start":{"line":25,"column":54},"end":{"line":26,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":36,"column":10},"end":{"line":36,"column":35}},"loc":{"start":{"line":37,"column":39},"end":{"line":85,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":47,"column":19},"end":{"line":47,"column":24}},"loc":{"start":{"line":47,"column":30},"end":{"line":54,"column":9}}}},"branchMap":{"0":{"loc":{"start":{"line":39,"column":8},"end":{"line":41,"column":9}},"type":"if","locations":[{"start":{"line":39,"column":8},"end":{"line":41,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":49,"column":12},"end":{"line":53,"column":13}},"type":"if","locations":[{"start":{"line":49,"column":12},"end":{"line":53,"column":13}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":37,"column":15},"end":{"line":37,"column":null}},"type":"cond-expr","locations":[{"start":{"line":37,"column":39},"end":{"line":37,"column":null}},{"start":{"line":37,"column":39},"end":{"line":37,"column":null}}]},"3":{"loc":{"start":{"line":37,"column":15},"end":{"line":37,"column":39}},"type":"binary-expr","locations":[{"start":{"line":37,"column":15},"end":{"line":37,"column":null}},{"start":{"line":37,"column":15},"end":{"line":37,"column":39}}]},"4":{"loc":{"start":{"line":38,"column":7},"end":{"line":38,"column":null}},"type":"cond-expr","locations":[{"start":{"line":38,"column":14},"end":{"line":38,"column":null}},{"start":{"line":38,"column":14},"end":{"line":38,"column":null}}]},"5":{"loc":{"start":{"line":38,"column":7},"end":{"line":38,"column":14}},"type":"binary-expr","locations":[{"start":{"line":38,"column":7},"end":{"line":38,"column":null}},{"start":{"line":38,"column":7},"end":{"line":38,"column":14}}]},"6":{"loc":{"start":{"line":22,"column":52},"end":{"line":22,"column":null}},"type":"cond-expr","locations":[{"start":{"line":22,"column":77},"end":{"line":22,"column":null}},{"start":{"line":22,"column":77},"end":{"line":22,"column":null}}]},"7":{"loc":{"start":{"line":22,"column":52},"end":{"line":22,"column":77}},"type":"binary-expr","locations":[{"start":{"line":22,"column":52},"end":{"line":22,"column":null}},{"start":{"line":22,"column":52},"end":{"line":22,"column":77}}]},"8":{"loc":{"start":{"line":22,"column":77},"end":{"line":23,"column":null}},"type":"cond-expr","locations":[{"start":{"line":23,"column":51},"end":{"line":23,"column":null}},{"start":{"line":23,"column":51},"end":{"line":23,"column":null}}]},"9":{"loc":{"start":{"line":22,"column":77},"end":{"line":23,"column":51}},"type":"binary-expr","locations":[{"start":{"line":22,"column":77},"end":{"line":23,"column":null}},{"start":{"line":23,"column":39},"end":{"line":23,"column":51}}]},"10":{"loc":{"start":{"line":23,"column":51},"end":{"line":25,"column":null}},"type":"cond-expr","locations":[{"start":{"line":25,"column":54},"end":{"line":25,"column":null}},{"start":{"line":25,"column":54},"end":{"line":25,"column":null}}]},"11":{"loc":{"start":{"line":23,"column":51},"end":{"line":25,"column":54}},"type":"binary-expr","locations":[{"start":{"line":23,"column":51},"end":{"line":25,"column":null}},{"start":{"line":25,"column":34},"end":{"line":25,"column":54}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":3,"9":3,"10":3,"11":3,"12":3,"13":1,"14":2,"15":2,"16":0,"17":0,"18":0,"19":2,"20":2,"21":2,"22":2,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1},"f":{"0":3,"1":3,"2":0},"b":{"0":[1,2],"1":[0,0],"2":[1,0],"3":[1,1],"4":[1,0],"5":[1,1],"6":[1,0],"7":[1,1],"8":[1,0],"9":[1,1],"10":[0,1],"11":[1,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/listeners/review-stage.listener.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/listeners/review-stage.listener.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":51}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":52}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":48}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":117}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":89}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":63}},"6":{"start":{"line":14,"column":32},"end":{"line":69,"column":null}},"7":{"start":{"line":15,"column":30},"end":{"line":15,"column":66}},"8":{"start":{"line":18,"column":25},"end":{"line":18,"column":40}},"9":{"start":{"line":19,"column":25},"end":{"line":19,"column":40}},"10":{"start":{"line":33,"column":8},"end":{"line":35,"column":9}},"11":{"start":{"line":34,"column":12},"end":{"line":34,"column":19}},"12":{"start":{"line":37,"column":8},"end":{"line":39,"column":10}},"13":{"start":{"line":40,"column":8},"end":{"line":42,"column":10}},"14":{"start":{"line":44,"column":8},"end":{"line":67,"column":9}},"15":{"start":{"line":45,"column":12},"end":{"line":48,"column":14}},"16":{"start":{"line":50,"column":27},"end":{"line":51,"column":null}},"17":{"start":{"line":54,"column":12},"end":{"line":56,"column":14}},"18":{"start":{"line":58,"column":12},"end":{"line":61,"column":14}},"19":{"start":{"line":63,"column":12},"end":{"line":66,"column":14}},"20":{"start":{"line":14,"column":13},"end":{"line":14,"column":32}},"21":{"start":{"line":30,"column":10},"end":{"line":68,"column":null}},"22":{"start":{"line":14,"column":13},"end":{"line":69,"column":null}}},"fnMap":{"0":{"name":"(anonymous_2)","decl":{"start":{"line":17,"column":4},"end":{"line":17,"column":null}},"loc":{"start":{"line":19,"column":53},"end":{"line":20,"column":8}}},"1":{"name":"(anonymous_3)","decl":{"start":{"line":30,"column":10},"end":{"line":30,"column":36}},"loc":{"start":{"line":31,"column":40},"end":{"line":68,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":33,"column":8},"end":{"line":35,"column":9}},"type":"if","locations":[{"start":{"line":33,"column":8},"end":{"line":35,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":31,"column":15},"end":{"line":31,"column":null}},"type":"cond-expr","locations":[{"start":{"line":31,"column":40},"end":{"line":31,"column":null}},{"start":{"line":31,"column":40},"end":{"line":31,"column":null}}]},"2":{"loc":{"start":{"line":31,"column":15},"end":{"line":31,"column":40}},"type":"binary-expr","locations":[{"start":{"line":31,"column":15},"end":{"line":31,"column":null}},{"start":{"line":31,"column":15},"end":{"line":31,"column":40}}]},"3":{"loc":{"start":{"line":32,"column":7},"end":{"line":32,"column":null}},"type":"cond-expr","locations":[{"start":{"line":32,"column":14},"end":{"line":32,"column":null}},{"start":{"line":32,"column":14},"end":{"line":32,"column":null}}]},"4":{"loc":{"start":{"line":32,"column":7},"end":{"line":32,"column":14}},"type":"binary-expr","locations":[{"start":{"line":32,"column":7},"end":{"line":32,"column":null}},{"start":{"line":32,"column":7},"end":{"line":32,"column":14}}]},"5":{"loc":{"start":{"line":18,"column":40},"end":{"line":18,"column":null}},"type":"cond-expr","locations":[{"start":{"line":18,"column":56},"end":{"line":18,"column":null}},{"start":{"line":18,"column":56},"end":{"line":18,"column":null}}]},"6":{"loc":{"start":{"line":18,"column":40},"end":{"line":18,"column":56}},"type":"binary-expr","locations":[{"start":{"line":18,"column":40},"end":{"line":18,"column":null}},{"start":{"line":18,"column":40},"end":{"line":18,"column":56}}]},"7":{"loc":{"start":{"line":18,"column":56},"end":{"line":19,"column":null}},"type":"cond-expr","locations":[{"start":{"line":19,"column":53},"end":{"line":19,"column":null}},{"start":{"line":19,"column":53},"end":{"line":19,"column":null}}]},"8":{"loc":{"start":{"line":18,"column":56},"end":{"line":19,"column":53}},"type":"binary-expr","locations":[{"start":{"line":18,"column":56},"end":{"line":19,"column":null}},{"start":{"line":19,"column":40},"end":{"line":19,"column":53}}]}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":3,"8":3,"9":3,"10":3,"11":1,"12":2,"13":2,"14":2,"15":2,"16":2,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1},"f":{"0":3,"1":3},"b":{"0":[1,2],"1":[1,0],"2":[1,1],"3":[1,0],"4":[1,1],"5":[1,0],"6":[1,1],"7":[1,0],"8":[1,1]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/report-analytics.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/report-analytics.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":11},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/report-comment.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/report-comment.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":6},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/report-insight.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/report-insight.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":6},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/report.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/report.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":4,"column":71}}},"fnMap":{},"branchMap":{},"s":{"0":11},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/strategic-report-analytics.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/strategic-report-analytics.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":5},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/strategic-report-insight.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/strategic-report-insight.repository.port.ts","statementMap":{"0":{"start":{"line":3,"column":13},"end":{"line":5,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":5},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/strategic-report.repository.port.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/ports/strategic-report.repository.port.ts","statementMap":{"0":{"start":{"line":4,"column":13},"end":{"line":6,"column":2}}},"fnMap":{},"branchMap":{},"s":{"0":22},"f":{},"b":{}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-analytics.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-analytics.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":53}},"3":{"start":{"line":11,"column":7},"end":{"line":51,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":36}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":43}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":61}},"7":{"start":{"line":24,"column":26},"end":{"line":24,"column":71}},"8":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"9":{"start":{"line":27,"column":12},"end":{"line":29,"column":14}},"10":{"start":{"line":32,"column":8},"end":{"line":32,"column":25}},"11":{"start":{"line":39,"column":26},"end":{"line":39,"column":59}},"12":{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},"13":{"start":{"line":42,"column":12},"end":{"line":44,"column":14}},"14":{"start":{"line":47,"column":8},"end":{"line":47,"column":71}},"15":{"start":{"line":49,"column":8},"end":{"line":49,"column":25}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":35}},"17":{"start":{"line":11,"column":13},"end":{"line":51,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":59},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":33,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":9}},"loc":{"start":{"line":37,"column":26},"end":{"line":50,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"type":"if","locations":[{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},"type":"if","locations":[{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":14,"column":36},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":65},"end":{"line":14,"column":null}},{"start":{"line":14,"column":65},"end":{"line":14,"column":null}}]},"3":{"loc":{"start":{"line":14,"column":36},"end":{"line":14,"column":65}},"type":"binary-expr","locations":[{"start":{"line":14,"column":36},"end":{"line":14,"column":null}},{"start":{"line":14,"column":36},"end":{"line":14,"column":65}}]},"4":{"loc":{"start":{"line":14,"column":65},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":59},"end":{"line":15,"column":null}},{"start":{"line":15,"column":59},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":14,"column":65},"end":{"line":15,"column":59}},"type":"binary-expr","locations":[{"start":{"line":14,"column":65},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":59}}]}},"s":{"0":2,"1":2,"2":2,"3":2,"4":4,"5":4,"6":2,"7":2,"8":2,"9":1,"10":1,"11":2,"12":2,"13":1,"14":1,"15":1,"16":2,"17":2},"f":{"0":4,"1":2,"2":2},"b":{"0":[1,1],"1":[1,1],"2":[0,2],"3":[2,0],"4":[2,0],"5":[2,2]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-comment.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-comment.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":53}},"3":{"start":{"line":11,"column":7},"end":{"line":50,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":35}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":43}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":61}},"7":{"start":{"line":24,"column":25},"end":{"line":24,"column":69}},"8":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"9":{"start":{"line":27,"column":12},"end":{"line":29,"column":14}},"10":{"start":{"line":32,"column":8},"end":{"line":32,"column":24}},"11":{"start":{"line":36,"column":24},"end":{"line":36,"column":56}},"12":{"start":{"line":38,"column":8},"end":{"line":42,"column":9}},"13":{"start":{"line":39,"column":12},"end":{"line":41,"column":14}},"14":{"start":{"line":44,"column":8},"end":{"line":44,"column":23}},"15":{"start":{"line":48,"column":8},"end":{"line":48,"column":45}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":33}},"17":{"start":{"line":11,"column":13},"end":{"line":50,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":59},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":33,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":9}},"loc":{"start":{"line":35,"column":28},"end":{"line":45,"column":5}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":47,"column":4},"end":{"line":47,"column":9}},"loc":{"start":{"line":47,"column":45},"end":{"line":49,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"type":"if","locations":[{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":26,"column":12},"end":{"line":26,"column":46}},"type":"binary-expr","locations":[{"start":{"line":26,"column":12},"end":{"line":26,"column":21}},{"start":{"line":26,"column":25},"end":{"line":26,"column":46}}]},"2":{"loc":{"start":{"line":38,"column":8},"end":{"line":42,"column":9}},"type":"if","locations":[{"start":{"line":38,"column":8},"end":{"line":42,"column":9}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":62},"end":{"line":14,"column":null}},{"start":{"line":14,"column":62},"end":{"line":14,"column":null}}]},"4":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":62}},"type":"binary-expr","locations":[{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},{"start":{"line":14,"column":35},"end":{"line":14,"column":62}}]},"5":{"loc":{"start":{"line":14,"column":62},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":59},"end":{"line":15,"column":null}},{"start":{"line":15,"column":59},"end":{"line":15,"column":null}}]},"6":{"loc":{"start":{"line":14,"column":62},"end":{"line":15,"column":59}},"type":"binary-expr","locations":[{"start":{"line":14,"column":62},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":59}}]}},"s":{"0":2,"1":2,"2":2,"3":2,"4":6,"5":6,"6":3,"7":2,"8":2,"9":1,"10":1,"11":2,"12":2,"13":1,"14":1,"15":1,"16":2,"17":2},"f":{"0":6,"1":3,"2":2,"3":1},"b":{"0":[1,1],"1":[2,2],"2":[1,1],"3":[0,2],"4":[2,0],"5":[2,0],"6":[2,2]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-insight.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/report-insight.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":53}},"3":{"start":{"line":11,"column":7},"end":{"line":51,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":35}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":43}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":61}},"7":{"start":{"line":24,"column":25},"end":{"line":24,"column":69}},"8":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"9":{"start":{"line":27,"column":12},"end":{"line":29,"column":14}},"10":{"start":{"line":32,"column":8},"end":{"line":32,"column":24}},"11":{"start":{"line":39,"column":24},"end":{"line":39,"column":56}},"12":{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},"13":{"start":{"line":42,"column":12},"end":{"line":44,"column":14}},"14":{"start":{"line":47,"column":8},"end":{"line":47,"column":69}},"15":{"start":{"line":49,"column":8},"end":{"line":49,"column":23}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":33}},"17":{"start":{"line":11,"column":13},"end":{"line":51,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":59},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":33,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":35,"column":4},"end":{"line":35,"column":9}},"loc":{"start":{"line":37,"column":26},"end":{"line":50,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},"type":"if","locations":[{"start":{"line":26,"column":8},"end":{"line":30,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},"type":"if","locations":[{"start":{"line":41,"column":8},"end":{"line":45,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":62},"end":{"line":14,"column":null}},{"start":{"line":14,"column":62},"end":{"line":14,"column":null}}]},"3":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":62}},"type":"binary-expr","locations":[{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},{"start":{"line":14,"column":35},"end":{"line":14,"column":62}}]},"4":{"loc":{"start":{"line":14,"column":62},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":59},"end":{"line":15,"column":null}},{"start":{"line":15,"column":59},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":14,"column":62},"end":{"line":15,"column":59}},"type":"binary-expr","locations":[{"start":{"line":14,"column":62},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":59}}]}},"s":{"0":2,"1":2,"2":2,"3":2,"4":4,"5":4,"6":2,"7":2,"8":2,"9":1,"10":1,"11":2,"12":2,"13":1,"14":1,"15":1,"16":2,"17":2},"f":{"0":4,"1":2,"2":2},"b":{"0":[1,1],"1":[1,1],"2":[0,2],"3":[2,0],"4":[2,0],"5":[2,2]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/reports.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/reports.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":16,"column":0},"end":{"line":16,"column":null}},"2":{"start":{"line":23,"column":0},"end":{"line":23,"column":33}},"3":{"start":{"line":24,"column":0},"end":{"line":24,"column":null}},"4":{"start":{"line":28,"column":0},"end":{"line":28,"column":null}},"5":{"start":{"line":33,"column":0},"end":{"line":33,"column":null}},"6":{"start":{"line":37,"column":0},"end":{"line":37,"column":null}},"7":{"start":{"line":41,"column":0},"end":{"line":41,"column":null}},"8":{"start":{"line":45,"column":0},"end":{"line":45,"column":null}},"9":{"start":{"line":49,"column":0},"end":{"line":49,"column":null}},"10":{"start":{"line":54,"column":0},"end":{"line":54,"column":86}},"11":{"start":{"line":58,"column":0},"end":{"line":58,"column":null}},"12":{"start":{"line":63,"column":0},"end":{"line":63,"column":77}},"13":{"start":{"line":64,"column":0},"end":{"line":64,"column":73}},"14":{"start":{"line":65,"column":0},"end":{"line":65,"column":58}},"15":{"start":{"line":66,"column":0},"end":{"line":66,"column":null}},"16":{"start":{"line":70,"column":0},"end":{"line":70,"column":null}},"17":{"start":{"line":74,"column":0},"end":{"line":74,"column":null}},"18":{"start":{"line":78,"column":0},"end":{"line":78,"column":null}},"19":{"start":{"line":84,"column":29},"end":{"line":1291,"column":null}},"20":{"start":{"line":85,"column":30},"end":{"line":85,"column":63}},"21":{"start":{"line":89,"column":25},"end":{"line":89,"column":34}},"22":{"start":{"line":91,"column":25},"end":{"line":91,"column":40}},"23":{"start":{"line":93,"column":25},"end":{"line":93,"column":39}},"24":{"start":{"line":95,"column":25},"end":{"line":95,"column":39}},"25":{"start":{"line":97,"column":25},"end":{"line":97,"column":38}},"26":{"start":{"line":99,"column":25},"end":{"line":99,"column":36}},"27":{"start":{"line":101,"column":25},"end":{"line":101,"column":34}},"28":{"start":{"line":103,"column":25},"end":{"line":103,"column":50}},"29":{"start":{"line":105,"column":25},"end":{"line":105,"column":34}},"30":{"start":{"line":107,"column":25},"end":{"line":107,"column":35}},"31":{"start":{"line":109,"column":25},"end":{"line":109,"column":40}},"32":{"start":{"line":111,"column":25},"end":{"line":111,"column":37}},"33":{"start":{"line":114,"column":36},"end":{"line":114,"column":73}},"34":{"start":{"line":120,"column":8},"end":{"line":120,"column":50}},"35":{"start":{"line":121,"column":8},"end":{"line":121,"column":48}},"36":{"start":{"line":129,"column":8},"end":{"line":129,"column":27}},"37":{"start":{"line":129,"column":20},"end":{"line":129,"column":27}},"38":{"start":{"line":132,"column":12},"end":{"line":133,"column":51}},"39":{"start":{"line":135,"column":8},"end":{"line":139,"column":9}},"40":{"start":{"line":136,"column":12},"end":{"line":138,"column":14}},"41":{"start":{"line":143,"column":23},"end":{"line":143,"column":54}},"42":{"start":{"line":145,"column":8},"end":{"line":147,"column":9}},"43":{"start":{"line":146,"column":12},"end":{"line":146,"column":78}},"44":{"start":{"line":149,"column":8},"end":{"line":149,"column":54}},"45":{"start":{"line":151,"column":8},"end":{"line":151,"column":22}},"46":{"start":{"line":163,"column":8},"end":{"line":163,"column":27}},"47":{"start":{"line":163,"column":20},"end":{"line":163,"column":27}},"48":{"start":{"line":166,"column":12},"end":{"line":167,"column":51}},"49":{"start":{"line":169,"column":8},"end":{"line":169,"column":32}},"50":{"start":{"line":169,"column":25},"end":{"line":169,"column":32}},"51":{"start":{"line":171,"column":23},"end":{"line":171,"column":67}},"52":{"start":{"line":173,"column":8},"end":{"line":176,"column":9}},"53":{"start":{"line":174,"column":12},"end":{"line":175,"column":51}},"54":{"start":{"line":178,"column":34},"end":{"line":178,"column":63}},"55":{"start":{"line":179,"column":32},"end":{"line":179,"column":59}},"56":{"start":{"line":181,"column":8},"end":{"line":181,"column":57}},"57":{"start":{"line":181,"column":50},"end":{"line":181,"column":57}},"58":{"start":{"line":183,"column":26},"end":{"line":185,"column":10}},"59":{"start":{"line":187,"column":8},"end":{"line":187,"column":41}},"60":{"start":{"line":187,"column":34},"end":{"line":187,"column":41}},"61":{"start":{"line":189,"column":8},"end":{"line":191,"column":10}},"62":{"start":{"line":198,"column":23},"end":{"line":198,"column":66}},"63":{"start":{"line":199,"column":8},"end":{"line":203,"column":9}},"64":{"start":{"line":200,"column":12},"end":{"line":202,"column":14}},"65":{"start":{"line":205,"column":8},"end":{"line":205,"column":54}},"66":{"start":{"line":207,"column":8},"end":{"line":207,"column":22}},"67":{"start":{"line":217,"column":8},"end":{"line":217,"column":78}},"68":{"start":{"line":219,"column":25},"end":{"line":219,"column":68}},"69":{"start":{"line":220,"column":8},"end":{"line":223,"column":9}},"70":{"start":{"line":221,"column":12},"end":{"line":221,"column":78}},"71":{"start":{"line":222,"column":12},"end":{"line":222,"column":28}},"72":{"start":{"line":225,"column":23},"end":{"line":225,"column":60}},"73":{"start":{"line":226,"column":8},"end":{"line":228,"column":9}},"74":{"start":{"line":227,"column":12},"end":{"line":227,"column":80}},"75":{"start":{"line":230,"column":31},"end":{"line":232,"column":null}},"76":{"start":{"line":234,"column":32},"end":{"line":234,"column":53}},"77":{"start":{"line":235,"column":59},"end":{"line":245,"column":10}},"78":{"start":{"line":238,"column":32},"end":{"line":238,"column":42}},"79":{"start":{"line":241,"column":28},"end":{"line":242,"column":60}},"80":{"start":{"line":247,"column":12},"end":{"line":247,"column":78}},"81":{"start":{"line":249,"column":26},"end":{"line":249,"column":27}},"82":{"start":{"line":250,"column":8},"end":{"line":252,"column":11}},"83":{"start":{"line":251,"column":12},"end":{"line":251,"column":40}},"84":{"start":{"line":254,"column":8},"end":{"line":256,"column":9}},"85":{"start":{"line":255,"column":12},"end":{"line":255,"column":71}},"86":{"start":{"line":258,"column":40},"end":{"line":263,"column":null}},"87":{"start":{"line":260,"column":16},"end":{"line":260,"column":60}},"88":{"start":{"line":261,"column":16},"end":{"line":261,"column":27}},"89":{"start":{"line":266,"column":28},"end":{"line":269,"column":null}},"90":{"start":{"line":271,"column":29},"end":{"line":274,"column":null}},"91":{"start":{"line":277,"column":8},"end":{"line":279,"column":10}},"92":{"start":{"line":281,"column":24},"end":{"line":284,"column":10}},"93":{"start":{"line":286,"column":26},"end":{"line":288,"column":null}},"94":{"start":{"line":291,"column":25},"end":{"line":291,"column":76}},"95":{"start":{"line":293,"column":12},"end":{"line":293,"column":68}},"96":{"start":{"line":295,"column":33},"end":{"line":297,"column":null}},"97":{"start":{"line":300,"column":23},"end":{"line":327,"column":10}},"98":{"start":{"line":329,"column":30},"end":{"line":329,"column":63}},"99":{"start":{"line":331,"column":8},"end":{"line":334,"column":11}},"100":{"start":{"line":336,"column":8},"end":{"line":341,"column":10}},"101":{"start":{"line":343,"column":8},"end":{"line":345,"column":10}},"102":{"start":{"line":347,"column":33},"end":{"line":350,"column":null}},"103":{"start":{"line":353,"column":35},"end":{"line":356,"column":null}},"104":{"start":{"line":359,"column":8},"end":{"line":362,"column":11}},"105":{"start":{"line":364,"column":8},"end":{"line":366,"column":10}},"106":{"start":{"line":368,"column":8},"end":{"line":370,"column":10}},"107":{"start":{"line":372,"column":8},"end":{"line":374,"column":11}},"108":{"start":{"line":376,"column":8},"end":{"line":376,"column":29}},"109":{"start":{"line":396,"column":8},"end":{"line":401,"column":9}},"110":{"start":{"line":397,"column":26},"end":{"line":397,"column":72}},"111":{"start":{"line":398,"column":12},"end":{"line":398,"column":64}},"112":{"start":{"line":400,"column":12},"end":{"line":400,"column":75}},"113":{"start":{"line":406,"column":14},"end":{"line":406,"column":16}},"114":{"start":{"line":408,"column":8},"end":{"line":416,"column":9}},"115":{"start":{"line":413,"column":12},"end":{"line":415,"column":14}},"116":{"start":{"line":418,"column":8},"end":{"line":429,"column":11}},"117":{"start":{"line":419,"column":12},"end":{"line":423,"column":13}},"118":{"start":{"line":422,"column":16},"end":{"line":422,"column":23}},"119":{"start":{"line":425,"column":12},"end":{"line":428,"column":15}},"120":{"start":{"line":431,"column":8},"end":{"line":435,"column":9}},"121":{"start":{"line":431,"column":51},"end":{"line":431,"column":60}},"122":{"start":{"line":432,"column":12},"end":{"line":434,"column":14}},"123":{"start":{"line":451,"column":37},"end":{"line":451,"column":62}},"124":{"start":{"line":452,"column":8},"end":{"line":463,"column":9}},"125":{"start":{"line":453,"column":12},"end":{"line":462,"column":13}},"126":{"start":{"line":458,"column":16},"end":{"line":461,"column":18}},"127":{"start":{"line":465,"column":39},"end":{"line":465,"column":64}},"128":{"start":{"line":466,"column":8},"end":{"line":480,"column":9}},"129":{"start":{"line":467,"column":12},"end":{"line":470,"column":25}},"130":{"start":{"line":470,"column":16},"end":{"line":470,"column":25}},"131":{"start":{"line":471,"column":33},"end":{"line":471,"column":76}},"132":{"start":{"line":472,"column":12},"end":{"line":472,"column":53}},"133":{"start":{"line":472,"column":44},"end":{"line":472,"column":53}},"134":{"start":{"line":473,"column":12},"end":{"line":477,"column":25}},"135":{"start":{"line":477,"column":16},"end":{"line":477,"column":25}},"136":{"start":{"line":478,"column":28},"end":{"line":478,"column":73}},"137":{"start":{"line":479,"column":12},"end":{"line":479,"column":66}},"138":{"start":{"line":482,"column":8},"end":{"line":519,"column":9}},"139":{"start":{"line":483,"column":33},"end":{"line":483,"column":54}},"140":{"start":{"line":484,"column":12},"end":{"line":484,"column":78}},"141":{"start":{"line":484,"column":69},"end":{"line":484,"column":78}},"142":{"start":{"line":486,"column":31},"end":{"line":488,"column":null}},"143":{"start":{"line":490,"column":33},"end":{"line":490,"column":62}},"144":{"start":{"line":491,"column":12},"end":{"line":491,"column":48}},"145":{"start":{"line":491,"column":39},"end":{"line":491,"column":48}},"146":{"start":{"line":493,"column":29},"end":{"line":493,"column":73}},"147":{"start":{"line":494,"column":12},"end":{"line":494,"column":43}},"148":{"start":{"line":494,"column":34},"end":{"line":494,"column":43}},"149":{"start":{"line":496,"column":35},"end":{"line":498,"column":null}},"150":{"start":{"line":500,"column":12},"end":{"line":505,"column":13}},"151":{"start":{"line":501,"column":16},"end":{"line":503,"column":18}},"152":{"start":{"line":504,"column":16},"end":{"line":504,"column":25}},"153":{"start":{"line":507,"column":33},"end":{"line":507,"column":78}},"154":{"start":{"line":509,"column":12},"end":{"line":518,"column":14}},"155":{"start":{"line":532,"column":8},"end":{"line":532,"column":70}},"156":{"start":{"line":545,"column":8},"end":{"line":558,"column":9}},"157":{"start":{"line":546,"column":26},"end":{"line":546,"column":57}},"158":{"start":{"line":547,"column":26},"end":{"line":547,"column":57}},"159":{"start":{"line":548,"column":31},"end":{"line":549,"column":null}},"160":{"start":{"line":551,"column":31},"end":{"line":551,"column":64}},"161":{"start":{"line":552,"column":31},"end":{"line":554,"column":39}},"162":{"start":{"line":555,"column":12},"end":{"line":557,"column":13}},"163":{"start":{"line":556,"column":16},"end":{"line":556,"column":31}},"164":{"start":{"line":559,"column":8},"end":{"line":559,"column":20}},"165":{"start":{"line":571,"column":29},"end":{"line":573,"column":68}},"166":{"start":{"line":572,"column":50},"end":{"line":572,"column":79}},"167":{"start":{"line":573,"column":25},"end":{"line":573,"column":66}},"168":{"start":{"line":574,"column":8},"end":{"line":574,"column":51}},"169":{"start":{"line":574,"column":39},"end":{"line":574,"column":51}},"170":{"start":{"line":575,"column":20},"end":{"line":577,"column":null}},"171":{"start":{"line":576,"column":28},"end":{"line":576,"column":43}},"172":{"start":{"line":579,"column":8},"end":{"line":579,"column":50}},"173":{"start":{"line":590,"column":8},"end":{"line":590,"column":63}},"174":{"start":{"line":590,"column":51},"end":{"line":590,"column":63}},"175":{"start":{"line":592,"column":12},"end":{"line":592,"column":65}},"176":{"start":{"line":593,"column":8},"end":{"line":593,"column":47}},"177":{"start":{"line":609,"column":36},"end":{"line":610,"column":null}},"178":{"start":{"line":610,"column":28},"end":{"line":610,"column":60}},"179":{"start":{"line":613,"column":8},"end":{"line":615,"column":9}},"180":{"start":{"line":614,"column":12},"end":{"line":614,"column":24}},"181":{"start":{"line":617,"column":8},"end":{"line":619,"column":9}},"182":{"start":{"line":618,"column":12},"end":{"line":618,"column":34}},"183":{"start":{"line":621,"column":24},"end":{"line":623,"column":23}},"184":{"start":{"line":625,"column":8},"end":{"line":625,"column":42}},"185":{"start":{"line":645,"column":35},"end":{"line":646,"column":null}},"186":{"start":{"line":646,"column":19},"end":{"line":646,"column":62}},"187":{"start":{"line":649,"column":34},"end":{"line":651,"column":null}},"188":{"start":{"line":653,"column":59},"end":{"line":653,"column":61}},"189":{"start":{"line":654,"column":39},"end":{"line":654,"column":79}},"190":{"start":{"line":655,"column":48},"end":{"line":655,"column":50}},"191":{"start":{"line":656,"column":48},"end":{"line":656,"column":50}},"192":{"start":{"line":657,"column":49},"end":{"line":657,"column":51}},"193":{"start":{"line":659,"column":8},"end":{"line":757,"column":9}},"194":{"start":{"line":661,"column":16},"end":{"line":661,"column":64}},"195":{"start":{"line":663,"column":34},"end":{"line":665,"column":null}},"196":{"start":{"line":667,"column":34},"end":{"line":669,"column":null}},"197":{"start":{"line":671,"column":35},"end":{"line":673,"column":null}},"198":{"start":{"line":676,"column":12},"end":{"line":678,"column":13}},"199":{"start":{"line":677,"column":16},"end":{"line":677,"column":57}},"200":{"start":{"line":679,"column":12},"end":{"line":681,"column":13}},"201":{"start":{"line":680,"column":16},"end":{"line":680,"column":57}},"202":{"start":{"line":682,"column":12},"end":{"line":684,"column":13}},"203":{"start":{"line":683,"column":16},"end":{"line":683,"column":59}},"204":{"start":{"line":686,"column":37},"end":{"line":688,"column":null}},"205":{"start":{"line":690,"column":37},"end":{"line":692,"column":null}},"206":{"start":{"line":694,"column":38},"end":{"line":696,"column":null}},"207":{"start":{"line":699,"column":42},"end":{"line":701,"column":null}},"208":{"start":{"line":703,"column":43},"end":{"line":705,"column":null}},"209":{"start":{"line":708,"column":12},"end":{"line":731,"column":14}},"210":{"start":{"line":733,"column":33},"end":{"line":733,"column":54}},"211":{"start":{"line":734,"column":12},"end":{"line":756,"column":13}},"212":{"start":{"line":735,"column":36},"end":{"line":743,"column":18}},"213":{"start":{"line":745,"column":16},"end":{"line":747,"column":17}},"214":{"start":{"line":746,"column":20},"end":{"line":746,"column":63}},"215":{"start":{"line":748,"column":16},"end":{"line":750,"column":17}},"216":{"start":{"line":749,"column":20},"end":{"line":749,"column":63}},"217":{"start":{"line":751,"column":16},"end":{"line":753,"column":17}},"218":{"start":{"line":752,"column":20},"end":{"line":752,"column":65}},"219":{"start":{"line":755,"column":16},"end":{"line":755,"column":70}},"220":{"start":{"line":759,"column":61},"end":{"line":759,"column":63}},"221":{"start":{"line":760,"column":50},"end":{"line":760,"column":52}},"222":{"start":{"line":761,"column":50},"end":{"line":761,"column":52}},"223":{"start":{"line":762,"column":51},"end":{"line":762,"column":53}},"224":{"start":{"line":764,"column":8},"end":{"line":831,"column":9}},"225":{"start":{"line":765,"column":34},"end":{"line":766,"column":null}},"226":{"start":{"line":766,"column":50},"end":{"line":766,"column":64}},"227":{"start":{"line":768,"column":34},"end":{"line":769,"column":null}},"228":{"start":{"line":769,"column":50},"end":{"line":769,"column":64}},"229":{"start":{"line":771,"column":35},"end":{"line":772,"column":null}},"230":{"start":{"line":772,"column":51},"end":{"line":772,"column":65}},"231":{"start":{"line":775,"column":12},"end":{"line":777,"column":13}},"232":{"start":{"line":776,"column":16},"end":{"line":776,"column":59}},"233":{"start":{"line":778,"column":12},"end":{"line":780,"column":13}},"234":{"start":{"line":779,"column":16},"end":{"line":779,"column":59}},"235":{"start":{"line":781,"column":12},"end":{"line":783,"column":13}},"236":{"start":{"line":782,"column":16},"end":{"line":782,"column":61}},"237":{"start":{"line":785,"column":37},"end":{"line":787,"column":null}},"238":{"start":{"line":789,"column":37},"end":{"line":791,"column":null}},"239":{"start":{"line":793,"column":38},"end":{"line":795,"column":null}},"240":{"start":{"line":798,"column":42},"end":{"line":800,"column":null}},"241":{"start":{"line":802,"column":43},"end":{"line":804,"column":null}},"242":{"start":{"line":807,"column":12},"end":{"line":830,"column":14}},"243":{"start":{"line":833,"column":8},"end":{"line":848,"column":10}},"244":{"start":{"line":861,"column":35},"end":{"line":867,"column":null}},"245":{"start":{"line":865,"column":24},"end":{"line":865,"column":74}},"246":{"start":{"line":867,"column":35},"end":{"line":867,"column":54}},"247":{"start":{"line":869,"column":24},"end":{"line":869,"column":57}},"248":{"start":{"line":870,"column":8},"end":{"line":877,"column":9}},"249":{"start":{"line":871,"column":12},"end":{"line":873,"column":13}},"250":{"start":{"line":872,"column":16},"end":{"line":872,"column":25}},"251":{"start":{"line":874,"column":27},"end":{"line":874,"column":63}},"252":{"start":{"line":875,"column":12},"end":{"line":875,"column":32}},"253":{"start":{"line":876,"column":12},"end":{"line":876,"column":51}},"254":{"start":{"line":878,"column":8},"end":{"line":878,"column":23}},"255":{"start":{"line":891,"column":23},"end":{"line":898,"column":65}},"256":{"start":{"line":894,"column":20},"end":{"line":896,"column":55}},"257":{"start":{"line":898,"column":29},"end":{"line":898,"column":64}},"258":{"start":{"line":899,"column":8},"end":{"line":899,"column":45}},"259":{"start":{"line":913,"column":8},"end":{"line":915,"column":9}},"260":{"start":{"line":914,"column":12},"end":{"line":914,"column":24}},"261":{"start":{"line":916,"column":8},"end":{"line":916,"column":57}},"262":{"start":{"line":929,"column":30},"end":{"line":930,"column":null}},"263":{"start":{"line":930,"column":36},"end":{"line":930,"column":64}},"264":{"start":{"line":932,"column":30},"end":{"line":933,"column":null}},"265":{"start":{"line":933,"column":36},"end":{"line":933,"column":54}},"266":{"start":{"line":935,"column":31},"end":{"line":936,"column":null}},"267":{"start":{"line":936,"column":36},"end":{"line":936,"column":55}},"268":{"start":{"line":939,"column":33},"end":{"line":941,"column":null}},"269":{"start":{"line":943,"column":33},"end":{"line":945,"column":null}},"270":{"start":{"line":947,"column":34},"end":{"line":949,"column":null}},"271":{"start":{"line":952,"column":38},"end":{"line":954,"column":null}},"272":{"start":{"line":956,"column":39},"end":{"line":958,"column":null}},"273":{"start":{"line":960,"column":8},"end":{"line":971,"column":10}},"274":{"start":{"line":985,"column":8},"end":{"line":987,"column":9}},"275":{"start":{"line":986,"column":12},"end":{"line":986,"column":24}},"276":{"start":{"line":988,"column":8},"end":{"line":988,"column":71}},"277":{"start":{"line":997,"column":8},"end":{"line":997,"column":40}},"278":{"start":{"line":997,"column":28},"end":{"line":997,"column":40}},"279":{"start":{"line":998,"column":8},"end":{"line":998,"column":60}},"280":{"start":{"line":1015,"column":30},"end":{"line":1015,"column":63}},"281":{"start":{"line":1016,"column":30},"end":{"line":1016,"column":63}},"282":{"start":{"line":1017,"column":31},"end":{"line":1017,"column":65}},"283":{"start":{"line":1019,"column":33},"end":{"line":1021,"column":null}},"284":{"start":{"line":1023,"column":33},"end":{"line":1025,"column":null}},"285":{"start":{"line":1027,"column":34},"end":{"line":1029,"column":null}},"286":{"start":{"line":1032,"column":38},"end":{"line":1034,"column":null}},"287":{"start":{"line":1036,"column":39},"end":{"line":1038,"column":null}},"288":{"start":{"line":1041,"column":8},"end":{"line":1052,"column":10}},"289":{"start":{"line":1067,"column":53},"end":{"line":1104,"column":14}},"290":{"start":{"line":1070,"column":20},"end":{"line":1072,"column":51}},"291":{"start":{"line":1074,"column":20},"end":{"line":1076,"column":51}},"292":{"start":{"line":1077,"column":16},"end":{"line":1077,"column":50}},"293":{"start":{"line":1080,"column":16},"end":{"line":1103,"column":18}},"294":{"start":{"line":1106,"column":29},"end":{"line":1108,"column":70}},"295":{"start":{"line":1107,"column":24},"end":{"line":1107,"column":39}},"296":{"start":{"line":1108,"column":40},"end":{"line":1108,"column":69}},"297":{"start":{"line":1109,"column":28},"end":{"line":1111,"column":70}},"298":{"start":{"line":1110,"column":24},"end":{"line":1110,"column":38}},"299":{"start":{"line":1111,"column":40},"end":{"line":1111,"column":69}},"300":{"start":{"line":1114,"column":12},"end":{"line":1114,"column":70}},"301":{"start":{"line":1116,"column":12},"end":{"line":1116,"column":70}},"302":{"start":{"line":1118,"column":12},"end":{"line":1118,"column":68}},"303":{"start":{"line":1120,"column":12},"end":{"line":1120,"column":68}},"304":{"start":{"line":1122,"column":58},"end":{"line":1147,"column":10}},"305":{"start":{"line":1126,"column":26},"end":{"line":1126,"column":77}},"306":{"start":{"line":1132,"column":26},"end":{"line":1132,"column":77}},"307":{"start":{"line":1138,"column":26},"end":{"line":1138,"column":75}},"308":{"start":{"line":1144,"column":26},"end":{"line":1144,"column":75}},"309":{"start":{"line":1149,"column":48},"end":{"line":1149,"column":50}},"310":{"start":{"line":1151,"column":8},"end":{"line":1177,"column":10}},"311":{"start":{"line":1179,"column":8},"end":{"line":1205,"column":10}},"312":{"start":{"line":1207,"column":8},"end":{"line":1233,"column":10}},"313":{"start":{"line":1235,"column":8},"end":{"line":1261,"column":10}},"314":{"start":{"line":1263,"column":8},"end":{"line":1263,"column":24}},"315":{"start":{"line":1271,"column":30},"end":{"line":1277,"column":5}},"316":{"start":{"line":1272,"column":28},"end":{"line":1273,"column":null}},"317":{"start":{"line":1273,"column":37},"end":{"line":1273,"column":70}},"318":{"start":{"line":1275,"column":8},"end":{"line":1275,"column":50}},"319":{"start":{"line":1275,"column":38},"end":{"line":1275,"column":50}},"320":{"start":{"line":1276,"column":8},"end":{"line":1276,"column":64}},"321":{"start":{"line":1285,"column":18},"end":{"line":1285,"column":28}},"322":{"start":{"line":1286,"column":8},"end":{"line":1288,"column":9}},"323":{"start":{"line":1287,"column":12},"end":{"line":1287,"column":34}},"324":{"start":{"line":1289,"column":8},"end":{"line":1289,"column":39}},"325":{"start":{"line":84,"column":13},"end":{"line":84,"column":29}},"326":{"start":{"line":84,"column":13},"end":{"line":1291,"column":null}}},"fnMap":{"0":{"name":"(anonymous_5)","decl":{"start":{"line":87,"column":4},"end":{"line":87,"column":null}},"loc":{"start":{"line":111,"column":56},"end":{"line":112,"column":8}}},"1":{"name":"(anonymous_6)","decl":{"start":{"line":116,"column":4},"end":{"line":116,"column":9}},"loc":{"start":{"line":118,"column":26},"end":{"line":122,"column":5}}},"2":{"name":"(anonymous_7)","decl":{"start":{"line":128,"column":4},"end":{"line":128,"column":9}},"loc":{"start":{"line":128,"column":52},"end":{"line":140,"column":5}}},"3":{"name":"(anonymous_8)","decl":{"start":{"line":142,"column":4},"end":{"line":142,"column":9}},"loc":{"start":{"line":142,"column":48},"end":{"line":152,"column":5}}},"4":{"name":"(anonymous_9)","decl":{"start":{"line":159,"column":4},"end":{"line":159,"column":9}},"loc":{"start":{"line":161,"column":26},"end":{"line":192,"column":5}}},"5":{"name":"(anonymous_10)","decl":{"start":{"line":194,"column":4},"end":{"line":194,"column":9}},"loc":{"start":{"line":196,"column":26},"end":{"line":208,"column":5}}},"6":{"name":"(anonymous_11)","decl":{"start":{"line":216,"column":4},"end":{"line":216,"column":9}},"loc":{"start":{"line":216,"column":50},"end":{"line":377,"column":5}}},"7":{"name":"(anonymous_12)","decl":{"start":{"line":238,"column":25},"end":{"line":238,"column":26}},"loc":{"start":{"line":238,"column":32},"end":{"line":238,"column":42}}},"8":{"name":"(anonymous_13)","decl":{"start":{"line":240,"column":24},"end":{"line":240,"column":25}},"loc":{"start":{"line":241,"column":28},"end":{"line":242,"column":60}}},"9":{"name":"(anonymous_14)","decl":{"start":{"line":250,"column":30},"end":{"line":250,"column":31}},"loc":{"start":{"line":250,"column":39},"end":{"line":252,"column":9}}},"10":{"name":"(anonymous_15)","decl":{"start":{"line":259,"column":12},"end":{"line":259,"column":13}},"loc":{"start":{"line":259,"column":26},"end":{"line":262,"column":13}}},"11":{"name":"(anonymous_16)","decl":{"start":{"line":387,"column":12},"end":{"line":387,"column":17}},"loc":{"start":{"line":392,"column":11},"end":{"line":436,"column":5}}},"12":{"name":"(anonymous_17)","decl":{"start":{"line":418,"column":30},"end":{"line":418,"column":31}},"loc":{"start":{"line":418,"column":39},"end":{"line":429,"column":9}}},"13":{"name":"(anonymous_18)","decl":{"start":{"line":431,"column":41},"end":{"line":431,"column":42}},"loc":{"start":{"line":431,"column":51},"end":{"line":431,"column":60}}},"14":{"name":"(anonymous_19)","decl":{"start":{"line":445,"column":12},"end":{"line":445,"column":17}},"loc":{"start":{"line":449,"column":28},"end":{"line":520,"column":5}}},"15":{"name":"(anonymous_20)","decl":{"start":{"line":528,"column":12},"end":{"line":528,"column":41}},"loc":{"start":{"line":530,"column":38},"end":{"line":533,"column":5}}},"16":{"name":"(anonymous_21)","decl":{"start":{"line":541,"column":12},"end":{"line":541,"column":31}},"loc":{"start":{"line":543,"column":33},"end":{"line":560,"column":5}}},"17":{"name":"(anonymous_22)","decl":{"start":{"line":568,"column":12},"end":{"line":568,"column":28}},"loc":{"start":{"line":569,"column":56},"end":{"line":580,"column":5}}},"18":{"name":"(anonymous_23)","decl":{"start":{"line":572,"column":20},"end":{"line":572,"column":21}},"loc":{"start":{"line":572,"column":50},"end":{"line":572,"column":79}}},"19":{"name":"(anonymous_24)","decl":{"start":{"line":573,"column":17},"end":{"line":573,"column":18}},"loc":{"start":{"line":573,"column":25},"end":{"line":573,"column":66}}},"20":{"name":"(anonymous_25)","decl":{"start":{"line":576,"column":12},"end":{"line":576,"column":13}},"loc":{"start":{"line":576,"column":28},"end":{"line":576,"column":43}}},"21":{"name":"(anonymous_26)","decl":{"start":{"line":587,"column":12},"end":{"line":587,"column":24}},"loc":{"start":{"line":588,"column":50},"end":{"line":594,"column":5}}},"22":{"name":"(anonymous_27)","decl":{"start":{"line":604,"column":12},"end":{"line":604,"column":28}},"loc":{"start":{"line":607,"column":36},"end":{"line":626,"column":5}}},"23":{"name":"(anonymous_28)","decl":{"start":{"line":610,"column":12},"end":{"line":610,"column":13}},"loc":{"start":{"line":610,"column":28},"end":{"line":610,"column":60}}},"24":{"name":"(anonymous_29)","decl":{"start":{"line":635,"column":12},"end":{"line":635,"column":33}},"loc":{"start":{"line":638,"column":25},"end":{"line":849,"column":5}}},"25":{"name":"(anonymous_30)","decl":{"start":{"line":646,"column":12},"end":{"line":646,"column":13}},"loc":{"start":{"line":646,"column":19},"end":{"line":646,"column":62}}},"26":{"name":"(anonymous_31)","decl":{"start":{"line":766,"column":43},"end":{"line":766,"column":44}},"loc":{"start":{"line":766,"column":50},"end":{"line":766,"column":64}}},"27":{"name":"(anonymous_32)","decl":{"start":{"line":769,"column":43},"end":{"line":769,"column":44}},"loc":{"start":{"line":769,"column":50},"end":{"line":769,"column":64}}},"28":{"name":"(anonymous_33)","decl":{"start":{"line":772,"column":44},"end":{"line":772,"column":45}},"loc":{"start":{"line":772,"column":51},"end":{"line":772,"column":65}}},"29":{"name":"(anonymous_34)","decl":{"start":{"line":857,"column":12},"end":{"line":857,"column":34}},"loc":{"start":{"line":859,"column":49},"end":{"line":879,"column":5}}},"30":{"name":"(anonymous_35)","decl":{"start":{"line":864,"column":20},"end":{"line":864,"column":21}},"loc":{"start":{"line":865,"column":24},"end":{"line":865,"column":74}}},"31":{"name":"(anonymous_36)","decl":{"start":{"line":867,"column":21},"end":{"line":867,"column":22}},"loc":{"start":{"line":867,"column":35},"end":{"line":867,"column":54}}},"32":{"name":"(anonymous_37)","decl":{"start":{"line":887,"column":12},"end":{"line":887,"column":38}},"loc":{"start":{"line":889,"column":36},"end":{"line":900,"column":5}}},"33":{"name":"(anonymous_38)","decl":{"start":{"line":893,"column":16},"end":{"line":893,"column":17}},"loc":{"start":{"line":894,"column":20},"end":{"line":896,"column":55}}},"34":{"name":"(anonymous_39)","decl":{"start":{"line":898,"column":17},"end":{"line":898,"column":18}},"loc":{"start":{"line":898,"column":29},"end":{"line":898,"column":64}}},"35":{"name":"(anonymous_40)","decl":{"start":{"line":909,"column":12},"end":{"line":909,"column":32}},"loc":{"start":{"line":911,"column":28},"end":{"line":917,"column":5}}},"36":{"name":"(anonymous_41)","decl":{"start":{"line":925,"column":12},"end":{"line":925,"column":44}},"loc":{"start":{"line":927,"column":25},"end":{"line":972,"column":5}}},"37":{"name":"(anonymous_42)","decl":{"start":{"line":930,"column":26},"end":{"line":930,"column":27}},"loc":{"start":{"line":930,"column":36},"end":{"line":930,"column":64}}},"38":{"name":"(anonymous_43)","decl":{"start":{"line":933,"column":26},"end":{"line":933,"column":27}},"loc":{"start":{"line":933,"column":36},"end":{"line":933,"column":54}}},"39":{"name":"(anonymous_44)","decl":{"start":{"line":936,"column":26},"end":{"line":936,"column":27}},"loc":{"start":{"line":936,"column":36},"end":{"line":936,"column":55}}},"40":{"name":"(anonymous_45)","decl":{"start":{"line":981,"column":12},"end":{"line":981,"column":31}},"loc":{"start":{"line":983,"column":25},"end":{"line":989,"column":5}}},"41":{"name":"(anonymous_46)","decl":{"start":{"line":996,"column":12},"end":{"line":996,"column":27}},"loc":{"start":{"line":996,"column":49},"end":{"line":999,"column":5}}},"42":{"name":"(anonymous_47)","decl":{"start":{"line":1009,"column":12},"end":{"line":1009,"column":44}},"loc":{"start":{"line":1013,"column":25},"end":{"line":1053,"column":5}}},"43":{"name":"(anonymous_48)","decl":{"start":{"line":1062,"column":12},"end":{"line":1062,"column":35}},"loc":{"start":{"line":1065,"column":24},"end":{"line":1264,"column":5}}},"44":{"name":"(anonymous_49)","decl":{"start":{"line":1068,"column":18},"end":{"line":1068,"column":19}},"loc":{"start":{"line":1068,"column":27},"end":{"line":1078,"column":13}}},"45":{"name":"(anonymous_50)","decl":{"start":{"line":1079,"column":17},"end":{"line":1079,"column":18}},"loc":{"start":{"line":1079,"column":35},"end":{"line":1104,"column":13}}},"46":{"name":"(anonymous_51)","decl":{"start":{"line":1107,"column":17},"end":{"line":1107,"column":18}},"loc":{"start":{"line":1107,"column":24},"end":{"line":1107,"column":39}}},"47":{"name":"(anonymous_52)","decl":{"start":{"line":1108,"column":20},"end":{"line":1108,"column":21}},"loc":{"start":{"line":1108,"column":40},"end":{"line":1108,"column":69}}},"48":{"name":"(anonymous_53)","decl":{"start":{"line":1110,"column":17},"end":{"line":1110,"column":18}},"loc":{"start":{"line":1110,"column":24},"end":{"line":1110,"column":38}}},"49":{"name":"(anonymous_54)","decl":{"start":{"line":1111,"column":20},"end":{"line":1111,"column":21}},"loc":{"start":{"line":1111,"column":40},"end":{"line":1111,"column":69}}},"50":{"name":"(anonymous_55)","decl":{"start":{"line":1125,"column":36},"end":{"line":1125,"column":37}},"loc":{"start":{"line":1126,"column":26},"end":{"line":1126,"column":77}}},"51":{"name":"(anonymous_56)","decl":{"start":{"line":1131,"column":36},"end":{"line":1131,"column":37}},"loc":{"start":{"line":1132,"column":26},"end":{"line":1132,"column":77}}},"52":{"name":"(anonymous_57)","decl":{"start":{"line":1137,"column":36},"end":{"line":1137,"column":37}},"loc":{"start":{"line":1138,"column":26},"end":{"line":1138,"column":75}}},"53":{"name":"(anonymous_58)","decl":{"start":{"line":1143,"column":36},"end":{"line":1143,"column":37}},"loc":{"start":{"line":1144,"column":26},"end":{"line":1144,"column":75}}},"54":{"name":"(anonymous_59)","decl":{"start":{"line":1271,"column":30},"end":{"line":1271,"column":31}},"loc":{"start":{"line":1271,"column":70},"end":{"line":1277,"column":5}}},"55":{"name":"(anonymous_60)","decl":{"start":{"line":1273,"column":12},"end":{"line":1273,"column":13}},"loc":{"start":{"line":1273,"column":37},"end":{"line":1273,"column":70}}},"56":{"name":"(anonymous_61)","decl":{"start":{"line":1284,"column":12},"end":{"line":1284,"column":42}},"loc":{"start":{"line":1284,"column":61},"end":{"line":1290,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":129,"column":8},"end":{"line":129,"column":27}},"type":"if","locations":[{"start":{"line":129,"column":8},"end":{"line":129,"column":27}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":132,"column":12},"end":{"line":133,"column":51}},"type":"binary-expr","locations":[{"start":{"line":132,"column":12},"end":{"line":132,"column":54}},{"start":{"line":133,"column":12},"end":{"line":133,"column":51}}]},"2":{"loc":{"start":{"line":135,"column":8},"end":{"line":139,"column":9}},"type":"if","locations":[{"start":{"line":135,"column":8},"end":{"line":139,"column":9}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":145,"column":8},"end":{"line":147,"column":9}},"type":"if","locations":[{"start":{"line":145,"column":8},"end":{"line":147,"column":9}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":163,"column":8},"end":{"line":163,"column":27}},"type":"if","locations":[{"start":{"line":163,"column":8},"end":{"line":163,"column":27}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":166,"column":12},"end":{"line":167,"column":51}},"type":"binary-expr","locations":[{"start":{"line":166,"column":12},"end":{"line":166,"column":54}},{"start":{"line":167,"column":12},"end":{"line":167,"column":51}}]},"6":{"loc":{"start":{"line":169,"column":8},"end":{"line":169,"column":32}},"type":"if","locations":[{"start":{"line":169,"column":8},"end":{"line":169,"column":32}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":173,"column":8},"end":{"line":176,"column":9}},"type":"if","locations":[{"start":{"line":173,"column":8},"end":{"line":176,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":181,"column":8},"end":{"line":181,"column":57}},"type":"if","locations":[{"start":{"line":181,"column":8},"end":{"line":181,"column":57}},{"start":{},"end":{}}]},"9":{"loc":{"start":{"line":181,"column":12},"end":{"line":181,"column":48}},"type":"binary-expr","locations":[{"start":{"line":181,"column":12},"end":{"line":181,"column":29}},{"start":{"line":181,"column":33},"end":{"line":181,"column":48}}]},"10":{"loc":{"start":{"line":187,"column":8},"end":{"line":187,"column":41}},"type":"if","locations":[{"start":{"line":187,"column":8},"end":{"line":187,"column":41}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":199,"column":8},"end":{"line":203,"column":9}},"type":"if","locations":[{"start":{"line":199,"column":8},"end":{"line":203,"column":9}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":220,"column":8},"end":{"line":223,"column":9}},"type":"if","locations":[{"start":{"line":220,"column":8},"end":{"line":223,"column":9}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":226,"column":8},"end":{"line":228,"column":9}},"type":"if","locations":[{"start":{"line":226,"column":8},"end":{"line":228,"column":9}},{"start":{},"end":{}}]},"14":{"loc":{"start":{"line":254,"column":8},"end":{"line":256,"column":9}},"type":"if","locations":[{"start":{"line":254,"column":8},"end":{"line":256,"column":9}},{"start":{},"end":{}}]},"15":{"loc":{"start":{"line":268,"column":12},"end":{"line":268,"column":65}},"type":"binary-expr","locations":[{"start":{"line":268,"column":12},"end":{"line":268,"column":60}},{"start":{"line":268,"column":64},"end":{"line":268,"column":65}}]},"16":{"loc":{"start":{"line":273,"column":12},"end":{"line":273,"column":66}},"type":"binary-expr","locations":[{"start":{"line":273,"column":12},"end":{"line":273,"column":61}},{"start":{"line":273,"column":65},"end":{"line":273,"column":66}}]},"17":{"loc":{"start":{"line":396,"column":8},"end":{"line":401,"column":9}},"type":"if","locations":[{"start":{"line":396,"column":8},"end":{"line":401,"column":9}},{"start":{"line":399,"column":15},"end":{"line":401,"column":9}}]},"18":{"loc":{"start":{"line":408,"column":8},"end":{"line":416,"column":9}},"type":"if","locations":[{"start":{"line":408,"column":8},"end":{"line":416,"column":9}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":409,"column":12},"end":{"line":411,"column":50}},"type":"binary-expr","locations":[{"start":{"line":409,"column":12},"end":{"line":409,"column":38}},{"start":{"line":410,"column":12},"end":{"line":411,"column":50}}]},"20":{"loc":{"start":{"line":419,"column":12},"end":{"line":423,"column":13}},"type":"if","locations":[{"start":{"line":419,"column":12},"end":{"line":423,"column":13}},{"start":{},"end":{}}]},"21":{"loc":{"start":{"line":431,"column":8},"end":{"line":435,"column":9}},"type":"if","locations":[{"start":{"line":431,"column":8},"end":{"line":435,"column":9}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":453,"column":12},"end":{"line":462,"column":13}},"type":"if","locations":[{"start":{"line":453,"column":12},"end":{"line":462,"column":13}},{"start":{},"end":{}}]},"23":{"loc":{"start":{"line":454,"column":16},"end":{"line":456,"column":51}},"type":"binary-expr","locations":[{"start":{"line":454,"column":16},"end":{"line":454,"column":66}},{"start":{"line":455,"column":16},"end":{"line":455,"column":46}},{"start":{"line":456,"column":16},"end":{"line":456,"column":51}}]},"24":{"loc":{"start":{"line":467,"column":12},"end":{"line":470,"column":25}},"type":"if","locations":[{"start":{"line":467,"column":12},"end":{"line":470,"column":25}},{"start":{},"end":{}}]},"25":{"loc":{"start":{"line":472,"column":12},"end":{"line":472,"column":53}},"type":"if","locations":[{"start":{"line":472,"column":12},"end":{"line":472,"column":53}},{"start":{},"end":{}}]},"26":{"loc":{"start":{"line":473,"column":12},"end":{"line":477,"column":25}},"type":"if","locations":[{"start":{"line":473,"column":12},"end":{"line":477,"column":25}},{"start":{},"end":{}}]},"27":{"loc":{"start":{"line":474,"column":16},"end":{"line":475,"column":51}},"type":"binary-expr","locations":[{"start":{"line":474,"column":16},"end":{"line":474,"column":46}},{"start":{"line":475,"column":16},"end":{"line":475,"column":51}}]},"28":{"loc":{"start":{"line":478,"column":28},"end":{"line":478,"column":73}},"type":"binary-expr","locations":[{"start":{"line":478,"column":28},"end":{"line":478,"column":68}},{"start":{"line":478,"column":72},"end":{"line":478,"column":73}}]},"29":{"loc":{"start":{"line":484,"column":12},"end":{"line":484,"column":78}},"type":"if","locations":[{"start":{"line":484,"column":12},"end":{"line":484,"column":78}},{"start":{},"end":{}}]},"30":{"loc":{"start":{"line":484,"column":16},"end":{"line":484,"column":67}},"type":"binary-expr","locations":[{"start":{"line":484,"column":16},"end":{"line":484,"column":37}},{"start":{"line":484,"column":41},"end":{"line":484,"column":67}}]},"31":{"loc":{"start":{"line":487,"column":16},"end":{"line":487,"column":46}},"type":"binary-expr","locations":[{"start":{"line":487,"column":16},"end":{"line":487,"column":38}},{"start":{"line":487,"column":42},"end":{"line":487,"column":46}}]},"32":{"loc":{"start":{"line":488,"column":16},"end":{"line":488,"column":47}},"type":"binary-expr","locations":[{"start":{"line":488,"column":16},"end":{"line":488,"column":39}},{"start":{"line":488,"column":43},"end":{"line":488,"column":47}}]},"33":{"loc":{"start":{"line":491,"column":12},"end":{"line":491,"column":48}},"type":"if","locations":[{"start":{"line":491,"column":12},"end":{"line":491,"column":48}},{"start":{},"end":{}}]},"34":{"loc":{"start":{"line":494,"column":12},"end":{"line":494,"column":43}},"type":"if","locations":[{"start":{"line":494,"column":12},"end":{"line":494,"column":43}},{"start":{},"end":{}}]},"35":{"loc":{"start":{"line":500,"column":12},"end":{"line":505,"column":13}},"type":"if","locations":[{"start":{"line":500,"column":12},"end":{"line":505,"column":13}},{"start":{},"end":{}}]},"36":{"loc":{"start":{"line":507,"column":33},"end":{"line":507,"column":78}},"type":"binary-expr","locations":[{"start":{"line":507,"column":33},"end":{"line":507,"column":73}},{"start":{"line":507,"column":77},"end":{"line":507,"column":78}}]},"37":{"loc":{"start":{"line":511,"column":29},"end":{"line":511,"column":51}},"type":"binary-expr","locations":[{"start":{"line":511,"column":29},"end":{"line":511,"column":43}},{"start":{"line":511,"column":47},"end":{"line":511,"column":51}}]},"38":{"loc":{"start":{"line":552,"column":31},"end":{"line":554,"column":39}},"type":"cond-expr","locations":[{"start":{"line":553,"column":18},"end":{"line":553,"column":48}},{"start":{"line":554,"column":18},"end":{"line":554,"column":39}}]},"39":{"loc":{"start":{"line":555,"column":12},"end":{"line":557,"column":13}},"type":"if","locations":[{"start":{"line":555,"column":12},"end":{"line":557,"column":13}},{"start":{},"end":{}}]},"40":{"loc":{"start":{"line":555,"column":16},"end":{"line":555,"column":40}},"type":"binary-expr","locations":[{"start":{"line":555,"column":16},"end":{"line":555,"column":26}},{"start":{"line":555,"column":30},"end":{"line":555,"column":40}}]},"41":{"loc":{"start":{"line":572,"column":50},"end":{"line":572,"column":79}},"type":"binary-expr","locations":[{"start":{"line":572,"column":50},"end":{"line":572,"column":60}},{"start":{"line":572,"column":64},"end":{"line":572,"column":79}}]},"42":{"loc":{"start":{"line":573,"column":25},"end":{"line":573,"column":66}},"type":"cond-expr","locations":[{"start":{"line":573,"column":48},"end":{"line":573,"column":49}},{"start":{"line":573,"column":52},"end":{"line":573,"column":66}}]},"43":{"loc":{"start":{"line":574,"column":8},"end":{"line":574,"column":51}},"type":"if","locations":[{"start":{"line":574,"column":8},"end":{"line":574,"column":51}},{"start":{},"end":{}}]},"44":{"loc":{"start":{"line":590,"column":8},"end":{"line":590,"column":63}},"type":"if","locations":[{"start":{"line":590,"column":8},"end":{"line":590,"column":63}},{"start":{},"end":{}}]},"45":{"loc":{"start":{"line":590,"column":12},"end":{"line":590,"column":49}},"type":"binary-expr","locations":[{"start":{"line":590,"column":12},"end":{"line":590,"column":26}},{"start":{"line":590,"column":30},"end":{"line":590,"column":49}}]},"46":{"loc":{"start":{"line":592,"column":12},"end":{"line":592,"column":65}},"type":"cond-expr","locations":[{"start":{"line":592,"column":39},"end":{"line":592,"column":44}},{"start":{"line":592,"column":47},"end":{"line":592,"column":65}}]},"47":{"loc":{"start":{"line":613,"column":8},"end":{"line":615,"column":9}},"type":"if","locations":[{"start":{"line":613,"column":8},"end":{"line":615,"column":9}},{"start":{},"end":{}}]},"48":{"loc":{"start":{"line":617,"column":8},"end":{"line":619,"column":9}},"type":"if","locations":[{"start":{"line":617,"column":8},"end":{"line":619,"column":9}},{"start":{},"end":{}}]},"49":{"loc":{"start":{"line":661,"column":16},"end":{"line":661,"column":64}},"type":"binary-expr","locations":[{"start":{"line":661,"column":16},"end":{"line":661,"column":58}},{"start":{"line":661,"column":62},"end":{"line":661,"column":64}}]},"50":{"loc":{"start":{"line":676,"column":12},"end":{"line":678,"column":13}},"type":"if","locations":[{"start":{"line":676,"column":12},"end":{"line":678,"column":13}},{"start":{},"end":{}}]},"51":{"loc":{"start":{"line":679,"column":12},"end":{"line":681,"column":13}},"type":"if","locations":[{"start":{"line":679,"column":12},"end":{"line":681,"column":13}},{"start":{},"end":{}}]},"52":{"loc":{"start":{"line":682,"column":12},"end":{"line":684,"column":13}},"type":"if","locations":[{"start":{"line":682,"column":12},"end":{"line":684,"column":13}},{"start":{},"end":{}}]},"53":{"loc":{"start":{"line":717,"column":24},"end":{"line":717,"column":64}},"type":"binary-expr","locations":[{"start":{"line":717,"column":24},"end":{"line":717,"column":56}},{"start":{"line":717,"column":60},"end":{"line":717,"column":64}}]},"54":{"loc":{"start":{"line":718,"column":35},"end":{"line":718,"column":75}},"type":"binary-expr","locations":[{"start":{"line":718,"column":35},"end":{"line":718,"column":67}},{"start":{"line":718,"column":71},"end":{"line":718,"column":75}}]},"55":{"loc":{"start":{"line":719,"column":36},"end":{"line":719,"column":77}},"type":"binary-expr","locations":[{"start":{"line":719,"column":36},"end":{"line":719,"column":69}},{"start":{"line":719,"column":73},"end":{"line":719,"column":77}}]},"56":{"loc":{"start":{"line":721,"column":24},"end":{"line":721,"column":67}},"type":"binary-expr","locations":[{"start":{"line":721,"column":24},"end":{"line":721,"column":59}},{"start":{"line":721,"column":63},"end":{"line":721,"column":67}}]},"57":{"loc":{"start":{"line":723,"column":24},"end":{"line":723,"column":67}},"type":"binary-expr","locations":[{"start":{"line":723,"column":24},"end":{"line":723,"column":59}},{"start":{"line":723,"column":63},"end":{"line":723,"column":67}}]},"58":{"loc":{"start":{"line":725,"column":24},"end":{"line":725,"column":68}},"type":"binary-expr","locations":[{"start":{"line":725,"column":24},"end":{"line":725,"column":60}},{"start":{"line":725,"column":64},"end":{"line":725,"column":68}}]},"59":{"loc":{"start":{"line":727,"column":24},"end":{"line":727,"column":72}},"type":"binary-expr","locations":[{"start":{"line":727,"column":24},"end":{"line":727,"column":64}},{"start":{"line":727,"column":68},"end":{"line":727,"column":72}}]},"60":{"loc":{"start":{"line":729,"column":24},"end":{"line":729,"column":73}},"type":"binary-expr","locations":[{"start":{"line":729,"column":24},"end":{"line":729,"column":65}},{"start":{"line":729,"column":69},"end":{"line":729,"column":73}}]},"61":{"loc":{"start":{"line":734,"column":12},"end":{"line":756,"column":13}},"type":"if","locations":[{"start":{"line":734,"column":12},"end":{"line":756,"column":13}},{"start":{},"end":{}}]},"62":{"loc":{"start":{"line":734,"column":16},"end":{"line":734,"column":67}},"type":"binary-expr","locations":[{"start":{"line":734,"column":16},"end":{"line":734,"column":37}},{"start":{"line":734,"column":41},"end":{"line":734,"column":67}}]},"63":{"loc":{"start":{"line":735,"column":36},"end":{"line":743,"column":18}},"type":"binary-expr","locations":[{"start":{"line":735,"column":36},"end":{"line":736,"column":null}},{"start":{"line":737,"column":21},"end":{"line":743,"column":18}}]},"64":{"loc":{"start":{"line":739,"column":37},"end":{"line":739,"column":69}},"type":"binary-expr","locations":[{"start":{"line":739,"column":37},"end":{"line":739,"column":61}},{"start":{"line":739,"column":65},"end":{"line":739,"column":69}}]},"65":{"loc":{"start":{"line":745,"column":16},"end":{"line":747,"column":17}},"type":"if","locations":[{"start":{"line":745,"column":16},"end":{"line":747,"column":17}},{"start":{},"end":{}}]},"66":{"loc":{"start":{"line":748,"column":16},"end":{"line":750,"column":17}},"type":"if","locations":[{"start":{"line":748,"column":16},"end":{"line":750,"column":17}},{"start":{},"end":{}}]},"67":{"loc":{"start":{"line":751,"column":16},"end":{"line":753,"column":17}},"type":"if","locations":[{"start":{"line":751,"column":16},"end":{"line":753,"column":17}},{"start":{},"end":{}}]},"68":{"loc":{"start":{"line":775,"column":12},"end":{"line":777,"column":13}},"type":"if","locations":[{"start":{"line":775,"column":12},"end":{"line":777,"column":13}},{"start":{},"end":{}}]},"69":{"loc":{"start":{"line":778,"column":12},"end":{"line":780,"column":13}},"type":"if","locations":[{"start":{"line":778,"column":12},"end":{"line":780,"column":13}},{"start":{},"end":{}}]},"70":{"loc":{"start":{"line":781,"column":12},"end":{"line":783,"column":13}},"type":"if","locations":[{"start":{"line":781,"column":12},"end":{"line":783,"column":13}},{"start":{},"end":{}}]},"71":{"loc":{"start":{"line":816,"column":24},"end":{"line":816,"column":64}},"type":"binary-expr","locations":[{"start":{"line":816,"column":24},"end":{"line":816,"column":56}},{"start":{"line":816,"column":60},"end":{"line":816,"column":64}}]},"72":{"loc":{"start":{"line":817,"column":35},"end":{"line":817,"column":75}},"type":"binary-expr","locations":[{"start":{"line":817,"column":35},"end":{"line":817,"column":67}},{"start":{"line":817,"column":71},"end":{"line":817,"column":75}}]},"73":{"loc":{"start":{"line":818,"column":36},"end":{"line":818,"column":77}},"type":"binary-expr","locations":[{"start":{"line":818,"column":36},"end":{"line":818,"column":69}},{"start":{"line":818,"column":73},"end":{"line":818,"column":77}}]},"74":{"loc":{"start":{"line":820,"column":24},"end":{"line":820,"column":67}},"type":"binary-expr","locations":[{"start":{"line":820,"column":24},"end":{"line":820,"column":59}},{"start":{"line":820,"column":63},"end":{"line":820,"column":67}}]},"75":{"loc":{"start":{"line":822,"column":24},"end":{"line":822,"column":67}},"type":"binary-expr","locations":[{"start":{"line":822,"column":24},"end":{"line":822,"column":59}},{"start":{"line":822,"column":63},"end":{"line":822,"column":67}}]},"76":{"loc":{"start":{"line":824,"column":24},"end":{"line":824,"column":68}},"type":"binary-expr","locations":[{"start":{"line":824,"column":24},"end":{"line":824,"column":60}},{"start":{"line":824,"column":64},"end":{"line":824,"column":68}}]},"77":{"loc":{"start":{"line":826,"column":24},"end":{"line":826,"column":72}},"type":"binary-expr","locations":[{"start":{"line":826,"column":24},"end":{"line":826,"column":64}},{"start":{"line":826,"column":68},"end":{"line":826,"column":72}}]},"78":{"loc":{"start":{"line":828,"column":24},"end":{"line":828,"column":73}},"type":"binary-expr","locations":[{"start":{"line":828,"column":24},"end":{"line":828,"column":65}},{"start":{"line":828,"column":69},"end":{"line":828,"column":73}}]},"79":{"loc":{"start":{"line":871,"column":12},"end":{"line":873,"column":13}},"type":"if","locations":[{"start":{"line":871,"column":12},"end":{"line":873,"column":13}},{"start":{},"end":{}}]},"80":{"loc":{"start":{"line":874,"column":27},"end":{"line":874,"column":63}},"type":"binary-expr","locations":[{"start":{"line":874,"column":27},"end":{"line":874,"column":57}},{"start":{"line":874,"column":61},"end":{"line":874,"column":63}}]},"81":{"loc":{"start":{"line":894,"column":20},"end":{"line":896,"column":55}},"type":"binary-expr","locations":[{"start":{"line":894,"column":20},"end":{"line":894,"column":58}},{"start":{"line":895,"column":20},"end":{"line":895,"column":50}},{"start":{"line":896,"column":20},"end":{"line":896,"column":55}}]},"82":{"loc":{"start":{"line":913,"column":8},"end":{"line":915,"column":9}},"type":"if","locations":[{"start":{"line":913,"column":8},"end":{"line":915,"column":9}},{"start":{},"end":{}}]},"83":{"loc":{"start":{"line":913,"column":12},"end":{"line":913,"column":48}},"type":"binary-expr","locations":[{"start":{"line":913,"column":12},"end":{"line":913,"column":31}},{"start":{"line":913,"column":35},"end":{"line":913,"column":48}}]},"84":{"loc":{"start":{"line":985,"column":8},"end":{"line":987,"column":9}},"type":"if","locations":[{"start":{"line":985,"column":8},"end":{"line":987,"column":9}},{"start":{},"end":{}}]},"85":{"loc":{"start":{"line":985,"column":12},"end":{"line":985,"column":47}},"type":"binary-expr","locations":[{"start":{"line":985,"column":12},"end":{"line":985,"column":26}},{"start":{"line":985,"column":30},"end":{"line":985,"column":47}}]},"86":{"loc":{"start":{"line":997,"column":8},"end":{"line":997,"column":40}},"type":"if","locations":[{"start":{"line":997,"column":8},"end":{"line":997,"column":40}},{"start":{},"end":{}}]},"87":{"loc":{"start":{"line":1070,"column":20},"end":{"line":1072,"column":51}},"type":"cond-expr","locations":[{"start":{"line":1071,"column":27},"end":{"line":1071,"column":48}},{"start":{"line":1072,"column":27},"end":{"line":1072,"column":50}}]},"88":{"loc":{"start":{"line":1071,"column":27},"end":{"line":1071,"column":48}},"type":"binary-expr","locations":[{"start":{"line":1071,"column":27},"end":{"line":1071,"column":42}},{"start":{"line":1071,"column":46},"end":{"line":1071,"column":48}}]},"89":{"loc":{"start":{"line":1072,"column":27},"end":{"line":1072,"column":50}},"type":"binary-expr","locations":[{"start":{"line":1072,"column":27},"end":{"line":1072,"column":44}},{"start":{"line":1072,"column":48},"end":{"line":1072,"column":50}}]},"90":{"loc":{"start":{"line":1074,"column":20},"end":{"line":1076,"column":51}},"type":"cond-expr","locations":[{"start":{"line":1075,"column":27},"end":{"line":1075,"column":48}},{"start":{"line":1076,"column":27},"end":{"line":1076,"column":50}}]},"91":{"loc":{"start":{"line":1075,"column":27},"end":{"line":1075,"column":48}},"type":"binary-expr","locations":[{"start":{"line":1075,"column":27},"end":{"line":1075,"column":42}},{"start":{"line":1075,"column":46},"end":{"line":1075,"column":48}}]},"92":{"loc":{"start":{"line":1076,"column":27},"end":{"line":1076,"column":50}},"type":"binary-expr","locations":[{"start":{"line":1076,"column":27},"end":{"line":1076,"column":44}},{"start":{"line":1076,"column":48},"end":{"line":1076,"column":50}}]},"93":{"loc":{"start":{"line":1082,"column":24},"end":{"line":1084,"column":49}},"type":"cond-expr","locations":[{"start":{"line":1083,"column":30},"end":{"line":1083,"column":47}},{"start":{"line":1084,"column":30},"end":{"line":1084,"column":49}}]},"94":{"loc":{"start":{"line":1087,"column":24},"end":{"line":1089,"column":59}},"type":"binary-expr","locations":[{"start":{"line":1087,"column":25},"end":{"line":1089,"column":52}},{"start":{"line":1089,"column":57},"end":{"line":1089,"column":59}}]},"95":{"loc":{"start":{"line":1087,"column":25},"end":{"line":1089,"column":52}},"type":"cond-expr","locations":[{"start":{"line":1088,"column":30},"end":{"line":1088,"column":50}},{"start":{"line":1089,"column":30},"end":{"line":1089,"column":52}}]},"96":{"loc":{"start":{"line":1108,"column":40},"end":{"line":1108,"column":69}},"type":"binary-expr","locations":[{"start":{"line":1108,"column":40},"end":{"line":1108,"column":55}},{"start":{"line":1108,"column":59},"end":{"line":1108,"column":69}}]},"97":{"loc":{"start":{"line":1111,"column":40},"end":{"line":1111,"column":69}},"type":"binary-expr","locations":[{"start":{"line":1111,"column":40},"end":{"line":1111,"column":55}},{"start":{"line":1111,"column":59},"end":{"line":1111,"column":69}}]},"98":{"loc":{"start":{"line":1114,"column":12},"end":{"line":1114,"column":70}},"type":"cond-expr","locations":[{"start":{"line":1114,"column":38},"end":{"line":1114,"column":63}},{"start":{"line":1114,"column":66},"end":{"line":1114,"column":70}}]},"99":{"loc":{"start":{"line":1116,"column":12},"end":{"line":1116,"column":70}},"type":"cond-expr","locations":[{"start":{"line":1116,"column":38},"end":{"line":1116,"column":63}},{"start":{"line":1116,"column":66},"end":{"line":1116,"column":70}}]},"100":{"loc":{"start":{"line":1118,"column":12},"end":{"line":1118,"column":68}},"type":"cond-expr","locations":[{"start":{"line":1118,"column":37},"end":{"line":1118,"column":61}},{"start":{"line":1118,"column":64},"end":{"line":1118,"column":68}}]},"101":{"loc":{"start":{"line":1120,"column":12},"end":{"line":1120,"column":68}},"type":"cond-expr","locations":[{"start":{"line":1120,"column":37},"end":{"line":1120,"column":61}},{"start":{"line":1120,"column":64},"end":{"line":1120,"column":68}}]},"102":{"loc":{"start":{"line":1124,"column":16},"end":{"line":1128,"column":31}},"type":"cond-expr","locations":[{"start":{"line":1125,"column":22},"end":{"line":1126,"column":null}},{"start":{"line":1128,"column":22},"end":{"line":1128,"column":31}}]},"103":{"loc":{"start":{"line":1126,"column":38},"end":{"line":1126,"column":58}},"type":"binary-expr","locations":[{"start":{"line":1126,"column":38},"end":{"line":1126,"column":53}},{"start":{"line":1126,"column":57},"end":{"line":1126,"column":58}}]},"104":{"loc":{"start":{"line":1130,"column":16},"end":{"line":1134,"column":31}},"type":"cond-expr","locations":[{"start":{"line":1131,"column":22},"end":{"line":1132,"column":null}},{"start":{"line":1134,"column":22},"end":{"line":1134,"column":31}}]},"105":{"loc":{"start":{"line":1132,"column":38},"end":{"line":1132,"column":58}},"type":"binary-expr","locations":[{"start":{"line":1132,"column":38},"end":{"line":1132,"column":53}},{"start":{"line":1132,"column":57},"end":{"line":1132,"column":58}}]},"106":{"loc":{"start":{"line":1136,"column":16},"end":{"line":1140,"column":31}},"type":"cond-expr","locations":[{"start":{"line":1137,"column":22},"end":{"line":1138,"column":null}},{"start":{"line":1140,"column":22},"end":{"line":1140,"column":31}}]},"107":{"loc":{"start":{"line":1138,"column":38},"end":{"line":1138,"column":57}},"type":"binary-expr","locations":[{"start":{"line":1138,"column":38},"end":{"line":1138,"column":52}},{"start":{"line":1138,"column":56},"end":{"line":1138,"column":57}}]},"108":{"loc":{"start":{"line":1142,"column":16},"end":{"line":1146,"column":31}},"type":"cond-expr","locations":[{"start":{"line":1143,"column":22},"end":{"line":1144,"column":null}},{"start":{"line":1146,"column":22},"end":{"line":1146,"column":31}}]},"109":{"loc":{"start":{"line":1144,"column":38},"end":{"line":1144,"column":57}},"type":"binary-expr","locations":[{"start":{"line":1144,"column":38},"end":{"line":1144,"column":52}},{"start":{"line":1144,"column":56},"end":{"line":1144,"column":57}}]},"110":{"loc":{"start":{"line":1157,"column":20},"end":{"line":1159,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1158,"column":27},"end":{"line":1158,"column":75}},{"start":{"line":1159,"column":26},"end":{"line":1159,"column":30}}]},"111":{"loc":{"start":{"line":1158,"column":27},"end":{"line":1158,"column":75}},"type":"binary-expr","locations":[{"start":{"line":1158,"column":27},"end":{"line":1158,"column":67}},{"start":{"line":1158,"column":71},"end":{"line":1158,"column":75}}]},"112":{"loc":{"start":{"line":1161,"column":20},"end":{"line":1163,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1162,"column":27},"end":{"line":1162,"column":78}},{"start":{"line":1163,"column":26},"end":{"line":1163,"column":30}}]},"113":{"loc":{"start":{"line":1162,"column":27},"end":{"line":1162,"column":78}},"type":"binary-expr","locations":[{"start":{"line":1162,"column":27},"end":{"line":1162,"column":70}},{"start":{"line":1162,"column":74},"end":{"line":1162,"column":78}}]},"114":{"loc":{"start":{"line":1165,"column":20},"end":{"line":1167,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1166,"column":27},"end":{"line":1166,"column":75}},{"start":{"line":1167,"column":26},"end":{"line":1167,"column":30}}]},"115":{"loc":{"start":{"line":1166,"column":27},"end":{"line":1166,"column":75}},"type":"binary-expr","locations":[{"start":{"line":1166,"column":27},"end":{"line":1166,"column":67}},{"start":{"line":1166,"column":71},"end":{"line":1166,"column":75}}]},"116":{"loc":{"start":{"line":1169,"column":20},"end":{"line":1171,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1170,"column":27},"end":{"line":1170,"column":78}},{"start":{"line":1171,"column":26},"end":{"line":1171,"column":30}}]},"117":{"loc":{"start":{"line":1170,"column":27},"end":{"line":1170,"column":78}},"type":"binary-expr","locations":[{"start":{"line":1170,"column":27},"end":{"line":1170,"column":70}},{"start":{"line":1170,"column":74},"end":{"line":1170,"column":78}}]},"118":{"loc":{"start":{"line":1185,"column":20},"end":{"line":1187,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1186,"column":27},"end":{"line":1186,"column":74}},{"start":{"line":1187,"column":26},"end":{"line":1187,"column":30}}]},"119":{"loc":{"start":{"line":1186,"column":27},"end":{"line":1186,"column":74}},"type":"binary-expr","locations":[{"start":{"line":1186,"column":27},"end":{"line":1186,"column":66}},{"start":{"line":1186,"column":70},"end":{"line":1186,"column":74}}]},"120":{"loc":{"start":{"line":1189,"column":20},"end":{"line":1191,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1190,"column":27},"end":{"line":1190,"column":77}},{"start":{"line":1191,"column":26},"end":{"line":1191,"column":30}}]},"121":{"loc":{"start":{"line":1190,"column":27},"end":{"line":1190,"column":77}},"type":"binary-expr","locations":[{"start":{"line":1190,"column":27},"end":{"line":1190,"column":69}},{"start":{"line":1190,"column":73},"end":{"line":1190,"column":77}}]},"122":{"loc":{"start":{"line":1193,"column":20},"end":{"line":1195,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1194,"column":27},"end":{"line":1194,"column":74}},{"start":{"line":1195,"column":26},"end":{"line":1195,"column":30}}]},"123":{"loc":{"start":{"line":1194,"column":27},"end":{"line":1194,"column":74}},"type":"binary-expr","locations":[{"start":{"line":1194,"column":27},"end":{"line":1194,"column":66}},{"start":{"line":1194,"column":70},"end":{"line":1194,"column":74}}]},"124":{"loc":{"start":{"line":1197,"column":20},"end":{"line":1199,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1198,"column":27},"end":{"line":1198,"column":77}},{"start":{"line":1199,"column":26},"end":{"line":1199,"column":30}}]},"125":{"loc":{"start":{"line":1198,"column":27},"end":{"line":1198,"column":77}},"type":"binary-expr","locations":[{"start":{"line":1198,"column":27},"end":{"line":1198,"column":69}},{"start":{"line":1198,"column":73},"end":{"line":1198,"column":77}}]},"126":{"loc":{"start":{"line":1213,"column":20},"end":{"line":1215,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1214,"column":27},"end":{"line":1214,"column":74}},{"start":{"line":1215,"column":26},"end":{"line":1215,"column":30}}]},"127":{"loc":{"start":{"line":1214,"column":27},"end":{"line":1214,"column":74}},"type":"binary-expr","locations":[{"start":{"line":1214,"column":27},"end":{"line":1214,"column":66}},{"start":{"line":1214,"column":70},"end":{"line":1214,"column":74}}]},"128":{"loc":{"start":{"line":1217,"column":20},"end":{"line":1219,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1218,"column":27},"end":{"line":1218,"column":77}},{"start":{"line":1219,"column":26},"end":{"line":1219,"column":30}}]},"129":{"loc":{"start":{"line":1218,"column":27},"end":{"line":1218,"column":77}},"type":"binary-expr","locations":[{"start":{"line":1218,"column":27},"end":{"line":1218,"column":69}},{"start":{"line":1218,"column":73},"end":{"line":1218,"column":77}}]},"130":{"loc":{"start":{"line":1221,"column":20},"end":{"line":1223,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1222,"column":27},"end":{"line":1222,"column":74}},{"start":{"line":1223,"column":26},"end":{"line":1223,"column":30}}]},"131":{"loc":{"start":{"line":1222,"column":27},"end":{"line":1222,"column":74}},"type":"binary-expr","locations":[{"start":{"line":1222,"column":27},"end":{"line":1222,"column":66}},{"start":{"line":1222,"column":70},"end":{"line":1222,"column":74}}]},"132":{"loc":{"start":{"line":1225,"column":20},"end":{"line":1227,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1226,"column":27},"end":{"line":1226,"column":77}},{"start":{"line":1227,"column":26},"end":{"line":1227,"column":30}}]},"133":{"loc":{"start":{"line":1226,"column":27},"end":{"line":1226,"column":77}},"type":"binary-expr","locations":[{"start":{"line":1226,"column":27},"end":{"line":1226,"column":69}},{"start":{"line":1226,"column":73},"end":{"line":1226,"column":77}}]},"134":{"loc":{"start":{"line":1241,"column":20},"end":{"line":1243,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1242,"column":27},"end":{"line":1242,"column":73}},{"start":{"line":1243,"column":26},"end":{"line":1243,"column":30}}]},"135":{"loc":{"start":{"line":1242,"column":27},"end":{"line":1242,"column":73}},"type":"binary-expr","locations":[{"start":{"line":1242,"column":27},"end":{"line":1242,"column":65}},{"start":{"line":1242,"column":69},"end":{"line":1242,"column":73}}]},"136":{"loc":{"start":{"line":1245,"column":20},"end":{"line":1247,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1246,"column":27},"end":{"line":1246,"column":76}},{"start":{"line":1247,"column":26},"end":{"line":1247,"column":30}}]},"137":{"loc":{"start":{"line":1246,"column":27},"end":{"line":1246,"column":76}},"type":"binary-expr","locations":[{"start":{"line":1246,"column":27},"end":{"line":1246,"column":68}},{"start":{"line":1246,"column":72},"end":{"line":1246,"column":76}}]},"138":{"loc":{"start":{"line":1249,"column":20},"end":{"line":1251,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1250,"column":27},"end":{"line":1250,"column":73}},{"start":{"line":1251,"column":26},"end":{"line":1251,"column":30}}]},"139":{"loc":{"start":{"line":1250,"column":27},"end":{"line":1250,"column":73}},"type":"binary-expr","locations":[{"start":{"line":1250,"column":27},"end":{"line":1250,"column":65}},{"start":{"line":1250,"column":69},"end":{"line":1250,"column":73}}]},"140":{"loc":{"start":{"line":1253,"column":20},"end":{"line":1255,"column":30}},"type":"cond-expr","locations":[{"start":{"line":1254,"column":27},"end":{"line":1254,"column":76}},{"start":{"line":1255,"column":26},"end":{"line":1255,"column":30}}]},"141":{"loc":{"start":{"line":1254,"column":27},"end":{"line":1254,"column":76}},"type":"binary-expr","locations":[{"start":{"line":1254,"column":27},"end":{"line":1254,"column":68}},{"start":{"line":1254,"column":72},"end":{"line":1254,"column":76}}]},"142":{"loc":{"start":{"line":1273,"column":37},"end":{"line":1273,"column":70}},"type":"binary-expr","locations":[{"start":{"line":1273,"column":37},"end":{"line":1273,"column":49}},{"start":{"line":1273,"column":53},"end":{"line":1273,"column":70}}]},"143":{"loc":{"start":{"line":1275,"column":8},"end":{"line":1275,"column":50}},"type":"if","locations":[{"start":{"line":1275,"column":8},"end":{"line":1275,"column":50}},{"start":{},"end":{}}]},"144":{"loc":{"start":{"line":89,"column":34},"end":{"line":89,"column":null}},"type":"cond-expr","locations":[{"start":{"line":89,"column":54},"end":{"line":89,"column":null}},{"start":{"line":89,"column":54},"end":{"line":89,"column":null}}]},"145":{"loc":{"start":{"line":89,"column":34},"end":{"line":89,"column":54}},"type":"binary-expr","locations":[{"start":{"line":89,"column":34},"end":{"line":89,"column":null}},{"start":{"line":89,"column":34},"end":{"line":89,"column":54}}]},"146":{"loc":{"start":{"line":89,"column":54},"end":{"line":91,"column":null}},"type":"cond-expr","locations":[{"start":{"line":91,"column":69},"end":{"line":91,"column":null}},{"start":{"line":91,"column":69},"end":{"line":91,"column":null}}]},"147":{"loc":{"start":{"line":89,"column":54},"end":{"line":91,"column":69}},"type":"binary-expr","locations":[{"start":{"line":89,"column":54},"end":{"line":91,"column":null}},{"start":{"line":91,"column":40},"end":{"line":91,"column":69}}]},"148":{"loc":{"start":{"line":91,"column":69},"end":{"line":93,"column":null}},"type":"cond-expr","locations":[{"start":{"line":93,"column":66},"end":{"line":93,"column":null}},{"start":{"line":93,"column":66},"end":{"line":93,"column":null}}]},"149":{"loc":{"start":{"line":91,"column":69},"end":{"line":93,"column":66}},"type":"binary-expr","locations":[{"start":{"line":91,"column":69},"end":{"line":93,"column":null}},{"start":{"line":93,"column":39},"end":{"line":93,"column":66}}]},"150":{"loc":{"start":{"line":93,"column":66},"end":{"line":95,"column":null}},"type":"cond-expr","locations":[{"start":{"line":95,"column":66},"end":{"line":95,"column":null}},{"start":{"line":95,"column":66},"end":{"line":95,"column":null}}]},"151":{"loc":{"start":{"line":93,"column":66},"end":{"line":95,"column":66}},"type":"binary-expr","locations":[{"start":{"line":93,"column":66},"end":{"line":95,"column":null}},{"start":{"line":95,"column":39},"end":{"line":95,"column":66}}]},"152":{"loc":{"start":{"line":95,"column":66},"end":{"line":97,"column":null}},"type":"cond-expr","locations":[{"start":{"line":97,"column":62},"end":{"line":97,"column":null}},{"start":{"line":97,"column":62},"end":{"line":97,"column":null}}]},"153":{"loc":{"start":{"line":95,"column":66},"end":{"line":97,"column":62}},"type":"binary-expr","locations":[{"start":{"line":95,"column":66},"end":{"line":97,"column":null}},{"start":{"line":97,"column":38},"end":{"line":97,"column":62}}]},"154":{"loc":{"start":{"line":97,"column":62},"end":{"line":99,"column":null}},"type":"cond-expr","locations":[{"start":{"line":99,"column":58},"end":{"line":99,"column":null}},{"start":{"line":99,"column":58},"end":{"line":99,"column":null}}]},"155":{"loc":{"start":{"line":97,"column":62},"end":{"line":99,"column":58}},"type":"binary-expr","locations":[{"start":{"line":97,"column":62},"end":{"line":99,"column":null}},{"start":{"line":99,"column":36},"end":{"line":99,"column":58}}]},"156":{"loc":{"start":{"line":99,"column":58},"end":{"line":101,"column":null}},"type":"cond-expr","locations":[{"start":{"line":101,"column":54},"end":{"line":101,"column":null}},{"start":{"line":101,"column":54},"end":{"line":101,"column":null}}]},"157":{"loc":{"start":{"line":99,"column":58},"end":{"line":101,"column":54}},"type":"binary-expr","locations":[{"start":{"line":99,"column":58},"end":{"line":101,"column":null}},{"start":{"line":101,"column":34},"end":{"line":101,"column":54}}]},"158":{"loc":{"start":{"line":101,"column":54},"end":{"line":103,"column":null}},"type":"cond-expr","locations":[{"start":{"line":103,"column":86},"end":{"line":103,"column":null}},{"start":{"line":103,"column":86},"end":{"line":103,"column":null}}]},"159":{"loc":{"start":{"line":101,"column":54},"end":{"line":103,"column":86}},"type":"binary-expr","locations":[{"start":{"line":101,"column":54},"end":{"line":103,"column":null}},{"start":{"line":103,"column":50},"end":{"line":103,"column":86}}]},"160":{"loc":{"start":{"line":103,"column":86},"end":{"line":105,"column":null}},"type":"cond-expr","locations":[{"start":{"line":105,"column":54},"end":{"line":105,"column":null}},{"start":{"line":105,"column":54},"end":{"line":105,"column":null}}]},"161":{"loc":{"start":{"line":103,"column":86},"end":{"line":105,"column":54}},"type":"binary-expr","locations":[{"start":{"line":103,"column":86},"end":{"line":105,"column":null}},{"start":{"line":105,"column":34},"end":{"line":105,"column":54}}]},"162":{"loc":{"start":{"line":105,"column":54},"end":{"line":107,"column":null}},"type":"cond-expr","locations":[{"start":{"line":107,"column":56},"end":{"line":107,"column":null}},{"start":{"line":107,"column":56},"end":{"line":107,"column":null}}]},"163":{"loc":{"start":{"line":105,"column":54},"end":{"line":107,"column":56}},"type":"binary-expr","locations":[{"start":{"line":105,"column":54},"end":{"line":107,"column":null}},{"start":{"line":107,"column":35},"end":{"line":107,"column":56}}]},"164":{"loc":{"start":{"line":107,"column":56},"end":{"line":109,"column":null}},"type":"cond-expr","locations":[{"start":{"line":109,"column":66},"end":{"line":109,"column":null}},{"start":{"line":109,"column":66},"end":{"line":109,"column":null}}]},"165":{"loc":{"start":{"line":107,"column":56},"end":{"line":109,"column":66}},"type":"binary-expr","locations":[{"start":{"line":107,"column":56},"end":{"line":109,"column":null}},{"start":{"line":109,"column":40},"end":{"line":109,"column":66}}]},"166":{"loc":{"start":{"line":109,"column":66},"end":{"line":111,"column":null}},"type":"cond-expr","locations":[{"start":{"line":111,"column":56},"end":{"line":111,"column":null}},{"start":{"line":111,"column":56},"end":{"line":111,"column":null}}]},"167":{"loc":{"start":{"line":109,"column":66},"end":{"line":111,"column":56}},"type":"binary-expr","locations":[{"start":{"line":109,"column":66},"end":{"line":111,"column":null}},{"start":{"line":111,"column":37},"end":{"line":111,"column":56}}]}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":6,"8":6,"9":6,"10":6,"11":6,"12":6,"13":6,"14":6,"15":6,"16":6,"17":6,"18":6,"19":6,"20":15,"21":15,"22":15,"23":15,"24":15,"25":15,"26":15,"27":15,"28":15,"29":15,"30":15,"31":15,"32":15,"33":15,"34":3,"35":2,"36":3,"37":1,"38":2,"39":2,"40":1,"41":6,"42":6,"43":1,"44":5,"45":3,"46":6,"47":1,"48":5,"49":5,"50":1,"51":4,"52":4,"53":1,"54":3,"55":3,"56":3,"57":1,"58":2,"59":2,"60":1,"61":1,"62":2,"63":2,"64":1,"65":1,"66":1,"67":4,"68":4,"69":4,"70":1,"71":1,"72":3,"73":3,"74":1,"75":2,"76":2,"77":2,"78":1,"79":0,"80":2,"81":2,"82":2,"83":2,"84":2,"85":1,"86":1,"87":1,"88":1,"89":1,"90":1,"91":1,"92":1,"93":1,"94":1,"95":1,"96":1,"97":1,"98":1,"99":1,"100":1,"101":1,"102":1,"103":1,"104":1,"105":1,"106":1,"107":1,"108":1,"109":1,"110":1,"111":1,"112":0,"113":1,"114":1,"115":1,"116":0,"117":0,"118":0,"119":0,"120":0,"121":0,"122":0,"123":1,"124":1,"125":1,"126":1,"127":1,"128":1,"129":2,"130":1,"131":1,"132":1,"133":0,"134":1,"135":0,"136":1,"137":1,"138":1,"139":1,"140":1,"141":0,"142":1,"143":1,"144":1,"145":0,"146":1,"147":1,"148":0,"149":1,"150":1,"151":0,"152":0,"153":1,"154":1,"155":1,"156":1,"157":1,"158":1,"159":1,"160":1,"161":1,"162":1,"163":1,"164":0,"165":16,"166":13,"167":11,"168":16,"169":5,"170":11,"171":11,"172":11,"173":48,"174":6,"175":42,"176":42,"177":2,"178":2,"179":2,"180":1,"181":1,"182":0,"183":1,"184":1,"185":1,"186":1,"187":1,"188":1,"189":1,"190":1,"191":1,"192":1,"193":1,"194":1,"195":1,"196":1,"197":1,"198":1,"199":1,"200":1,"201":1,"202":1,"203":0,"204":1,"205":1,"206":1,"207":1,"208":1,"209":1,"210":1,"211":1,"212":1,"213":1,"214":1,"215":1,"216":1,"217":1,"218":0,"219":1,"220":1,"221":1,"222":1,"223":1,"224":1,"225":1,"226":1,"227":1,"228":1,"229":1,"230":0,"231":1,"232":1,"233":1,"234":1,"235":1,"236":0,"237":1,"238":1,"239":1,"240":1,"241":1,"242":1,"243":1,"244":1,"245":1,"246":1,"247":1,"248":1,"249":2,"250":0,"251":2,"252":2,"253":2,"254":1,"255":3,"256":6,"257":2,"258":3,"259":10,"260":5,"261":5,"262":1,"263":1,"264":1,"265":1,"266":1,"267":1,"268":1,"269":1,"270":1,"271":1,"272":1,"273":1,"274":15,"275":5,"276":10,"277":24,"278":9,"279":15,"280":2,"281":2,"282":2,"283":2,"284":2,"285":2,"286":2,"287":2,"288":2,"289":2,"290":0,"291":0,"292":0,"293":2,"294":2,"295":2,"296":2,"297":2,"298":2,"299":2,"300":2,"301":2,"302":2,"303":2,"304":2,"305":2,"306":2,"307":2,"308":2,"309":2,"310":2,"311":2,"312":2,"313":2,"314":2,"315":15,"316":6,"317":12,"318":6,"319":0,"320":6,"321":6,"322":6,"323":6,"324":6,"325":6,"326":6},"f":{"0":15,"1":3,"2":3,"3":6,"4":6,"5":2,"6":4,"7":1,"8":0,"9":2,"10":1,"11":1,"12":0,"13":0,"14":1,"15":1,"16":1,"17":16,"18":13,"19":11,"20":11,"21":48,"22":2,"23":2,"24":1,"25":1,"26":1,"27":1,"28":0,"29":1,"30":1,"31":1,"32":3,"33":6,"34":2,"35":10,"36":1,"37":1,"38":1,"39":1,"40":15,"41":24,"42":2,"43":2,"44":0,"45":2,"46":2,"47":2,"48":2,"49":2,"50":2,"51":2,"52":2,"53":2,"54":6,"55":12,"56":6},"b":{"0":[1,2],"1":[2,2],"2":[1,1],"3":[1,5],"4":[1,5],"5":[5,5],"6":[1,4],"7":[1,3],"8":[1,2],"9":[3,3],"10":[1,1],"11":[1,1],"12":[1,3],"13":[1,2],"14":[1,1],"15":[1,0],"16":[1,1],"17":[1,0],"18":[1,0],"19":[1,1],"20":[0,0],"21":[0,0],"22":[1,0],"23":[1,1,1],"24":[1,1],"25":[0,1],"26":[0,1],"27":[1,1],"28":[1,1],"29":[0,1],"30":[1,1],"31":[1,0],"32":[1,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[1,0],"37":[1,0],"38":[1,0],"39":[1,0],"40":[1,1],"41":[13,11],"42":[11,0],"43":[5,11],"44":[6,42],"45":[48,42],"46":[42,0],"47":[1,1],"48":[0,1],"49":[1,0],"50":[1,0],"51":[1,0],"52":[0,1],"53":[1,0],"54":[1,0],"55":[1,1],"56":[1,0],"57":[1,0],"58":[1,1],"59":[1,0],"60":[1,1],"61":[1,0],"62":[1,1],"63":[1,1],"64":[1,0],"65":[1,0],"66":[1,0],"67":[0,1],"68":[1,0],"69":[1,0],"70":[0,1],"71":[1,0],"72":[1,0],"73":[1,1],"74":[1,0],"75":[1,0],"76":[1,1],"77":[1,0],"78":[1,1],"79":[0,2],"80":[2,1],"81":[6,2,2],"82":[5,5],"83":[10,5],"84":[5,10],"85":[15,10],"86":[9,15],"87":[0,0],"88":[0,0],"89":[0,0],"90":[0,0],"91":[0,0],"92":[0,0],"93":[1,1],"94":[2,0],"95":[1,1],"96":[2,2],"97":[2,2],"98":[2,0],"99":[2,0],"100":[2,0],"101":[2,0],"102":[2,0],"103":[2,0],"104":[2,0],"105":[2,0],"106":[2,0],"107":[2,0],"108":[2,0],"109":[2,0],"110":[1,1],"111":[1,0],"112":[1,1],"113":[1,0],"114":[1,1],"115":[1,0],"116":[1,1],"117":[1,0],"118":[1,1],"119":[1,0],"120":[1,1],"121":[1,0],"122":[1,1],"123":[1,0],"124":[1,1],"125":[1,0],"126":[1,1],"127":[1,0],"128":[1,1],"129":[1,0],"130":[1,1],"131":[1,0],"132":[1,1],"133":[1,0],"134":[1,1],"135":[1,0],"136":[1,1],"137":[1,0],"138":[1,1],"139":[1,0],"140":[1,1],"141":[1,0],"142":[12,6],"143":[0,6],"144":[0,6],"145":[6,0],"146":[0,6],"147":[6,0],"148":[0,6],"149":[6,0],"150":[0,6],"151":[6,0],"152":[0,6],"153":[6,0],"154":[0,6],"155":[6,0],"156":[0,6],"157":[6,0],"158":[0,6],"159":[6,0],"160":[0,6],"161":[6,0],"162":[0,6],"163":[6,0],"164":[0,6],"165":[6,0],"166":[0,6],"167":[6,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/startegic-report-insight.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/startegic-report-insight.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":72}},"3":{"start":{"line":11,"column":7},"end":{"line":52,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":35}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":43}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":70}},"7":{"start":{"line":25,"column":12},"end":{"line":25,"column":74}},"8":{"start":{"line":27,"column":8},"end":{"line":31,"column":9}},"9":{"start":{"line":28,"column":12},"end":{"line":30,"column":14}},"10":{"start":{"line":33,"column":8},"end":{"line":33,"column":24}},"11":{"start":{"line":40,"column":24},"end":{"line":40,"column":56}},"12":{"start":{"line":42,"column":8},"end":{"line":46,"column":9}},"13":{"start":{"line":43,"column":12},"end":{"line":45,"column":14}},"14":{"start":{"line":48,"column":8},"end":{"line":48,"column":78}},"15":{"start":{"line":50,"column":8},"end":{"line":50,"column":23}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":42}},"17":{"start":{"line":11,"column":13},"end":{"line":52,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":68},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":34,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":36,"column":4},"end":{"line":36,"column":9}},"loc":{"start":{"line":38,"column":26},"end":{"line":51,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":27,"column":8},"end":{"line":31,"column":9}},"type":"if","locations":[{"start":{"line":27,"column":8},"end":{"line":31,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":42,"column":8},"end":{"line":46,"column":9}},"type":"if","locations":[{"start":{"line":42,"column":8},"end":{"line":46,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":71},"end":{"line":14,"column":null}},{"start":{"line":14,"column":71},"end":{"line":14,"column":null}}]},"3":{"loc":{"start":{"line":14,"column":35},"end":{"line":14,"column":71}},"type":"binary-expr","locations":[{"start":{"line":14,"column":35},"end":{"line":14,"column":null}},{"start":{"line":14,"column":35},"end":{"line":14,"column":71}}]},"4":{"loc":{"start":{"line":14,"column":71},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":68},"end":{"line":15,"column":null}},{"start":{"line":15,"column":68},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":14,"column":71},"end":{"line":15,"column":68}},"type":"binary-expr","locations":[{"start":{"line":14,"column":71},"end":{"line":15,"column":null}},{"start":{"line":15,"column":43},"end":{"line":15,"column":68}}]}},"s":{"0":2,"1":2,"2":2,"3":2,"4":4,"5":4,"6":2,"7":2,"8":2,"9":1,"10":1,"11":2,"12":2,"13":1,"14":1,"15":1,"16":2,"17":2},"f":{"0":4,"1":2,"2":2},"b":{"0":[1,1],"1":[1,1],"2":[0,2],"3":[2,0],"4":[2,0],"5":[2,2]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/strategic-report-analytics.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/strategic-report-analytics.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":71}},"1":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":72}},"3":{"start":{"line":11,"column":7},"end":{"line":57,"column":null}},"4":{"start":{"line":14,"column":25},"end":{"line":14,"column":45}},"5":{"start":{"line":15,"column":25},"end":{"line":15,"column":52}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":79}},"7":{"start":{"line":25,"column":12},"end":{"line":26,"column":null}},"8":{"start":{"line":29,"column":8},"end":{"line":33,"column":9}},"9":{"start":{"line":30,"column":12},"end":{"line":32,"column":14}},"10":{"start":{"line":35,"column":8},"end":{"line":35,"column":34}},"11":{"start":{"line":42,"column":35},"end":{"line":42,"column":77}},"12":{"start":{"line":44,"column":8},"end":{"line":48,"column":9}},"13":{"start":{"line":45,"column":12},"end":{"line":47,"column":14}},"14":{"start":{"line":50,"column":8},"end":{"line":53,"column":10}},"15":{"start":{"line":55,"column":8},"end":{"line":55,"column":34}},"16":{"start":{"line":11,"column":13},"end":{"line":11,"column":44}},"17":{"start":{"line":11,"column":13},"end":{"line":57,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":12,"column":4},"end":{"line":12,"column":null}},"loc":{"start":{"line":15,"column":77},"end":{"line":16,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":18,"column":4},"end":{"line":18,"column":9}},"loc":{"start":{"line":20,"column":26},"end":{"line":36,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":38,"column":4},"end":{"line":38,"column":9}},"loc":{"start":{"line":40,"column":26},"end":{"line":56,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":29,"column":8},"end":{"line":33,"column":9}},"type":"if","locations":[{"start":{"line":29,"column":8},"end":{"line":33,"column":9}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":44,"column":8},"end":{"line":48,"column":9}},"type":"if","locations":[{"start":{"line":44,"column":8},"end":{"line":48,"column":9}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":14,"column":45},"end":{"line":14,"column":null}},"type":"cond-expr","locations":[{"start":{"line":14,"column":83},"end":{"line":14,"column":null}},{"start":{"line":14,"column":83},"end":{"line":14,"column":null}}]},"3":{"loc":{"start":{"line":14,"column":45},"end":{"line":14,"column":83}},"type":"binary-expr","locations":[{"start":{"line":14,"column":45},"end":{"line":14,"column":null}},{"start":{"line":14,"column":45},"end":{"line":14,"column":83}}]},"4":{"loc":{"start":{"line":14,"column":83},"end":{"line":15,"column":null}},"type":"cond-expr","locations":[{"start":{"line":15,"column":77},"end":{"line":15,"column":null}},{"start":{"line":15,"column":77},"end":{"line":15,"column":null}}]},"5":{"loc":{"start":{"line":14,"column":83},"end":{"line":15,"column":77}},"type":"binary-expr","locations":[{"start":{"line":14,"column":83},"end":{"line":15,"column":null}},{"start":{"line":15,"column":52},"end":{"line":15,"column":77}}]}},"s":{"0":2,"1":2,"2":2,"3":2,"4":4,"5":4,"6":2,"7":2,"8":2,"9":1,"10":1,"11":2,"12":2,"13":1,"14":1,"15":1,"16":2,"17":2},"f":{"0":4,"1":2,"2":2},"b":{"0":[1,1],"1":[1,1],"2":[0,2],"3":[2,0],"4":[2,0],"5":[2,2]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/strategic-reports.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/strategic-reports.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":12,"column":0},"end":{"line":12,"column":null}},"2":{"start":{"line":19,"column":0},"end":{"line":19,"column":33}},"3":{"start":{"line":20,"column":0},"end":{"line":20,"column":null}},"4":{"start":{"line":24,"column":0},"end":{"line":24,"column":null}},"5":{"start":{"line":28,"column":0},"end":{"line":28,"column":null}},"6":{"start":{"line":34,"column":0},"end":{"line":34,"column":null}},"7":{"start":{"line":39,"column":0},"end":{"line":39,"column":null}},"8":{"start":{"line":44,"column":0},"end":{"line":44,"column":null}},"9":{"start":{"line":48,"column":0},"end":{"line":48,"column":null}},"10":{"start":{"line":54,"column":0},"end":{"line":54,"column":null}},"11":{"start":{"line":58,"column":0},"end":{"line":58,"column":null}},"12":{"start":{"line":62,"column":0},"end":{"line":62,"column":null}},"13":{"start":{"line":70,"column":0},"end":{"line":70,"column":92}},"14":{"start":{"line":71,"column":0},"end":{"line":71,"column":96}},"15":{"start":{"line":72,"column":0},"end":{"line":72,"column":77}},"16":{"start":{"line":73,"column":0},"end":{"line":73,"column":null}},"17":{"start":{"line":77,"column":0},"end":{"line":77,"column":null}},"18":{"start":{"line":81,"column":0},"end":{"line":81,"column":null}},"19":{"start":{"line":85,"column":0},"end":{"line":85,"column":null}},"20":{"start":{"line":89,"column":0},"end":{"line":89,"column":null}},"21":{"start":{"line":95,"column":38},"end":{"line":956,"column":null}},"22":{"start":{"line":96,"column":30},"end":{"line":96,"column":72}},"23":{"start":{"line":100,"column":25},"end":{"line":100,"column":43}},"24":{"start":{"line":102,"column":25},"end":{"line":102,"column":49}},"25":{"start":{"line":104,"column":25},"end":{"line":104,"column":38}},"26":{"start":{"line":106,"column":25},"end":{"line":106,"column":36}},"27":{"start":{"line":108,"column":25},"end":{"line":108,"column":34}},"28":{"start":{"line":110,"column":25},"end":{"line":110,"column":34}},"29":{"start":{"line":112,"column":25},"end":{"line":112,"column":37}},"30":{"start":{"line":114,"column":25},"end":{"line":114,"column":34}},"31":{"start":{"line":116,"column":25},"end":{"line":116,"column":32}},"32":{"start":{"line":118,"column":25},"end":{"line":118,"column":32}},"33":{"start":{"line":120,"column":25},"end":{"line":120,"column":36}},"34":{"start":{"line":122,"column":25},"end":{"line":122,"column":38}},"35":{"start":{"line":124,"column":25},"end":{"line":124,"column":36}},"36":{"start":{"line":126,"column":25},"end":{"line":126,"column":46}},"37":{"start":{"line":128,"column":25},"end":{"line":128,"column":39}},"38":{"start":{"line":135,"column":8},"end":{"line":135,"column":59}},"39":{"start":{"line":136,"column":8},"end":{"line":136,"column":57}},"40":{"start":{"line":144,"column":8},"end":{"line":144,"column":27}},"41":{"start":{"line":144,"column":20},"end":{"line":144,"column":27}},"42":{"start":{"line":147,"column":12},"end":{"line":148,"column":51}},"43":{"start":{"line":150,"column":8},"end":{"line":154,"column":9}},"44":{"start":{"line":151,"column":12},"end":{"line":153,"column":14}},"45":{"start":{"line":161,"column":23},"end":{"line":161,"column":63}},"46":{"start":{"line":163,"column":8},"end":{"line":167,"column":9}},"47":{"start":{"line":164,"column":12},"end":{"line":166,"column":14}},"48":{"start":{"line":169,"column":8},"end":{"line":169,"column":63}},"49":{"start":{"line":171,"column":8},"end":{"line":171,"column":22}},"50":{"start":{"line":183,"column":8},"end":{"line":183,"column":27}},"51":{"start":{"line":183,"column":20},"end":{"line":183,"column":27}},"52":{"start":{"line":186,"column":12},"end":{"line":187,"column":51}},"53":{"start":{"line":189,"column":8},"end":{"line":189,"column":32}},"54":{"start":{"line":189,"column":25},"end":{"line":189,"column":32}},"55":{"start":{"line":191,"column":24},"end":{"line":191,"column":78}},"56":{"start":{"line":193,"column":8},"end":{"line":196,"column":9}},"57":{"start":{"line":194,"column":12},"end":{"line":195,"column":61}},"58":{"start":{"line":198,"column":34},"end":{"line":199,"column":null}},"59":{"start":{"line":199,"column":24},"end":{"line":199,"column":53}},"60":{"start":{"line":202,"column":8},"end":{"line":202,"column":38}},"61":{"start":{"line":202,"column":31},"end":{"line":202,"column":38}},"62":{"start":{"line":204,"column":8},"end":{"line":206,"column":10}},"63":{"start":{"line":213,"column":23},"end":{"line":213,"column":73}},"64":{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},"65":{"start":{"line":215,"column":12},"end":{"line":217,"column":14}},"66":{"start":{"line":220,"column":8},"end":{"line":220,"column":63}},"67":{"start":{"line":222,"column":8},"end":{"line":222,"column":22}},"68":{"start":{"line":233,"column":8},"end":{"line":233,"column":77}},"69":{"start":{"line":235,"column":22},"end":{"line":235,"column":61}},"70":{"start":{"line":236,"column":8},"end":{"line":238,"column":9}},"71":{"start":{"line":237,"column":12},"end":{"line":237,"column":70}},"72":{"start":{"line":240,"column":25},"end":{"line":240,"column":75}},"73":{"start":{"line":241,"column":8},"end":{"line":244,"column":9}},"74":{"start":{"line":242,"column":12},"end":{"line":242,"column":76}},"75":{"start":{"line":243,"column":12},"end":{"line":243,"column":28}},"76":{"start":{"line":246,"column":24},"end":{"line":246,"column":62}},"77":{"start":{"line":247,"column":8},"end":{"line":251,"column":9}},"78":{"start":{"line":248,"column":12},"end":{"line":250,"column":14}},"79":{"start":{"line":253,"column":27},"end":{"line":253,"column":50}},"80":{"start":{"line":254,"column":26},"end":{"line":254,"column":43}},"81":{"start":{"line":261,"column":14},"end":{"line":261,"column":16}},"82":{"start":{"line":263,"column":8},"end":{"line":280,"column":9}},"83":{"start":{"line":264,"column":27},"end":{"line":264,"column":71}},"84":{"start":{"line":265,"column":12},"end":{"line":268,"column":13}},"85":{"start":{"line":266,"column":16},"end":{"line":266,"column":39}},"86":{"start":{"line":267,"column":16},"end":{"line":267,"column":42}},"87":{"start":{"line":270,"column":12},"end":{"line":279,"column":15}},"88":{"start":{"line":282,"column":26},"end":{"line":282,"column":47}},"89":{"start":{"line":283,"column":31},"end":{"line":283,"column":58}},"90":{"start":{"line":284,"column":27},"end":{"line":284,"column":50}},"91":{"start":{"line":285,"column":65},"end":{"line":289,"column":10}},"92":{"start":{"line":290,"column":29},"end":{"line":290,"column":54}},"93":{"start":{"line":291,"column":25},"end":{"line":291,"column":54}},"94":{"start":{"line":292,"column":29},"end":{"line":292,"column":62}},"95":{"start":{"line":293,"column":31},"end":{"line":293,"column":66}},"96":{"start":{"line":294,"column":29},"end":{"line":294,"column":62}},"97":{"start":{"line":296,"column":8},"end":{"line":415,"column":9}},"98":{"start":{"line":297,"column":28},"end":{"line":297,"column":45}},"99":{"start":{"line":298,"column":32},"end":{"line":298,"column":49}},"100":{"start":{"line":299,"column":34},"end":{"line":299,"column":51}},"101":{"start":{"line":300,"column":32},"end":{"line":300,"column":49}},"102":{"start":{"line":302,"column":26},"end":{"line":302,"column":67}},"103":{"start":{"line":303,"column":12},"end":{"line":313,"column":13}},"104":{"start":{"line":304,"column":16},"end":{"line":304,"column":37}},"105":{"start":{"line":306,"column":16},"end":{"line":308,"column":17}},"106":{"start":{"line":307,"column":20},"end":{"line":307,"column":46}},"107":{"start":{"line":310,"column":16},"end":{"line":312,"column":17}},"108":{"start":{"line":311,"column":20},"end":{"line":311,"column":54}},"109":{"start":{"line":315,"column":32},"end":{"line":317,"column":null}},"110":{"start":{"line":319,"column":12},"end":{"line":325,"column":15}},"111":{"start":{"line":320,"column":16},"end":{"line":320,"column":47}},"112":{"start":{"line":322,"column":16},"end":{"line":324,"column":17}},"113":{"start":{"line":323,"column":20},"end":{"line":323,"column":51}},"114":{"start":{"line":327,"column":12},"end":{"line":334,"column":13}},"115":{"start":{"line":328,"column":16},"end":{"line":333,"column":17}},"116":{"start":{"line":329,"column":33},"end":{"line":329,"column":66}},"117":{"start":{"line":330,"column":20},"end":{"line":332,"column":21}},"118":{"start":{"line":331,"column":24},"end":{"line":331,"column":51}},"119":{"start":{"line":336,"column":12},"end":{"line":343,"column":13}},"120":{"start":{"line":337,"column":16},"end":{"line":342,"column":17}},"121":{"start":{"line":338,"column":37},"end":{"line":338,"column":78}},"122":{"start":{"line":339,"column":20},"end":{"line":341,"column":21}},"123":{"start":{"line":340,"column":24},"end":{"line":340,"column":63}},"124":{"start":{"line":345,"column":28},"end":{"line":347,"column":14}},"125":{"start":{"line":349,"column":12},"end":{"line":355,"column":15}},"126":{"start":{"line":350,"column":16},"end":{"line":350,"column":39}},"127":{"start":{"line":352,"column":16},"end":{"line":354,"column":17}},"128":{"start":{"line":353,"column":20},"end":{"line":353,"column":55}},"129":{"start":{"line":358,"column":16},"end":{"line":359,"column":null}},"130":{"start":{"line":362,"column":12},"end":{"line":372,"column":15}},"131":{"start":{"line":363,"column":16},"end":{"line":371,"column":17}},"132":{"start":{"line":367,"column":20},"end":{"line":370,"column":26}},"133":{"start":{"line":374,"column":39},"end":{"line":377,"column":null}},"134":{"start":{"line":376,"column":20},"end":{"line":377,"column":54}},"135":{"start":{"line":380,"column":16},"end":{"line":380,"column":68}},"136":{"start":{"line":381,"column":35},"end":{"line":382,"column":null}},"137":{"start":{"line":382,"column":23},"end":{"line":382,"column":53}},"138":{"start":{"line":384,"column":12},"end":{"line":388,"column":13}},"139":{"start":{"line":385,"column":16},"end":{"line":387,"column":37}},"140":{"start":{"line":390,"column":12},"end":{"line":401,"column":13}},"141":{"start":{"line":391,"column":16},"end":{"line":396,"column":17}},"142":{"start":{"line":392,"column":37},"end":{"line":392,"column":78}},"143":{"start":{"line":393,"column":20},"end":{"line":395,"column":21}},"144":{"start":{"line":394,"column":24},"end":{"line":394,"column":63}},"145":{"start":{"line":397,"column":26},"end":{"line":397,"column":54}},"146":{"start":{"line":398,"column":16},"end":{"line":400,"column":17}},"147":{"start":{"line":399,"column":20},"end":{"line":399,"column":54}},"148":{"start":{"line":403,"column":12},"end":{"line":411,"column":13}},"149":{"start":{"line":404,"column":16},"end":{"line":410,"column":17}},"150":{"start":{"line":406,"column":24},"end":{"line":406,"column":69}},"151":{"start":{"line":407,"column":20},"end":{"line":409,"column":21}},"152":{"start":{"line":408,"column":24},"end":{"line":408,"column":69}},"153":{"start":{"line":413,"column":30},"end":{"line":413,"column":79}},"154":{"start":{"line":414,"column":12},"end":{"line":414,"column":72}},"155":{"start":{"line":414,"column":44},"end":{"line":414,"column":70}},"156":{"start":{"line":417,"column":35},"end":{"line":426,"column":null}},"157":{"start":{"line":419,"column":16},"end":{"line":425,"column":18}},"158":{"start":{"line":429,"column":25},"end":{"line":429,"column":76}},"159":{"start":{"line":430,"column":36},"end":{"line":433,"column":null}},"160":{"start":{"line":435,"column":33},"end":{"line":437,"column":null}},"161":{"start":{"line":440,"column":28},"end":{"line":442,"column":31}},"162":{"start":{"line":441,"column":57},"end":{"line":441,"column":62}},"163":{"start":{"line":442,"column":25},"end":{"line":442,"column":30}},"164":{"start":{"line":443,"column":33},"end":{"line":445,"column":31}},"165":{"start":{"line":444,"column":62},"end":{"line":444,"column":77}},"166":{"start":{"line":445,"column":25},"end":{"line":445,"column":30}},"167":{"start":{"line":446,"column":29},"end":{"line":446,"column":43}},"168":{"start":{"line":447,"column":8},"end":{"line":449,"column":11}},"169":{"start":{"line":448,"column":12},"end":{"line":448,"column":56}},"170":{"start":{"line":450,"column":31},"end":{"line":452,"column":31}},"171":{"start":{"line":451,"column":60},"end":{"line":451,"column":73}},"172":{"start":{"line":452,"column":25},"end":{"line":452,"column":30}},"173":{"start":{"line":453,"column":27},"end":{"line":453,"column":76}},"174":{"start":{"line":453,"column":70},"end":{"line":453,"column":75}},"175":{"start":{"line":454,"column":31},"end":{"line":455,"column":null}},"176":{"start":{"line":455,"column":22},"end":{"line":455,"column":27}},"177":{"start":{"line":457,"column":33},"end":{"line":458,"column":null}},"178":{"start":{"line":458,"column":22},"end":{"line":458,"column":27}},"179":{"start":{"line":460,"column":31},"end":{"line":461,"column":null}},"180":{"start":{"line":461,"column":22},"end":{"line":461,"column":27}},"181":{"start":{"line":464,"column":29},"end":{"line":466,"column":39}},"182":{"start":{"line":465,"column":32},"end":{"line":465,"column":56}},"183":{"start":{"line":467,"column":28},"end":{"line":469,"column":39}},"184":{"start":{"line":468,"column":32},"end":{"line":468,"column":55}},"185":{"start":{"line":470,"column":29},"end":{"line":472,"column":39}},"186":{"start":{"line":471,"column":32},"end":{"line":471,"column":56}},"187":{"start":{"line":474,"column":23},"end":{"line":506,"column":10}},"188":{"start":{"line":508,"column":30},"end":{"line":508,"column":72}},"189":{"start":{"line":510,"column":8},"end":{"line":512,"column":11}},"190":{"start":{"line":514,"column":8},"end":{"line":516,"column":10}},"191":{"start":{"line":518,"column":35},"end":{"line":520,"column":null}},"192":{"start":{"line":523,"column":8},"end":{"line":525,"column":11}},"193":{"start":{"line":527,"column":8},"end":{"line":529,"column":10}},"194":{"start":{"line":531,"column":27},"end":{"line":532,"column":null}},"195":{"start":{"line":534,"column":8},"end":{"line":538,"column":9}},"196":{"start":{"line":535,"column":12},"end":{"line":537,"column":14}},"197":{"start":{"line":540,"column":8},"end":{"line":542,"column":10}},"198":{"start":{"line":544,"column":8},"end":{"line":544,"column":26}},"199":{"start":{"line":556,"column":29},"end":{"line":558,"column":68}},"200":{"start":{"line":557,"column":50},"end":{"line":557,"column":79}},"201":{"start":{"line":558,"column":25},"end":{"line":558,"column":66}},"202":{"start":{"line":559,"column":8},"end":{"line":559,"column":51}},"203":{"start":{"line":559,"column":39},"end":{"line":559,"column":51}},"204":{"start":{"line":560,"column":20},"end":{"line":562,"column":null}},"205":{"start":{"line":561,"column":28},"end":{"line":561,"column":43}},"206":{"start":{"line":564,"column":8},"end":{"line":564,"column":50}},"207":{"start":{"line":575,"column":8},"end":{"line":575,"column":63}},"208":{"start":{"line":575,"column":51},"end":{"line":575,"column":63}},"209":{"start":{"line":577,"column":12},"end":{"line":577,"column":65}},"210":{"start":{"line":578,"column":8},"end":{"line":578,"column":47}},"211":{"start":{"line":592,"column":8},"end":{"line":594,"column":9}},"212":{"start":{"line":593,"column":12},"end":{"line":593,"column":34}},"213":{"start":{"line":596,"column":24},"end":{"line":596,"column":75}},"214":{"start":{"line":598,"column":8},"end":{"line":598,"column":60}},"215":{"start":{"line":617,"column":70},"end":{"line":617,"column":72}},"216":{"start":{"line":619,"column":8},"end":{"line":682,"column":9}},"217":{"start":{"line":620,"column":69},"end":{"line":620,"column":71}},"218":{"start":{"line":621,"column":12},"end":{"line":627,"column":15}},"219":{"start":{"line":622,"column":16},"end":{"line":626,"column":19}},"220":{"start":{"line":623,"column":20},"end":{"line":625,"column":21}},"221":{"start":{"line":624,"column":24},"end":{"line":624,"column":56}},"222":{"start":{"line":629,"column":34},"end":{"line":630,"column":null}},"223":{"start":{"line":630,"column":51},"end":{"line":630,"column":76}},"224":{"start":{"line":632,"column":34},"end":{"line":633,"column":null}},"225":{"start":{"line":633,"column":51},"end":{"line":633,"column":66}},"226":{"start":{"line":635,"column":35},"end":{"line":636,"column":null}},"227":{"start":{"line":636,"column":51},"end":{"line":636,"column":67}},"228":{"start":{"line":639,"column":37},"end":{"line":641,"column":null}},"229":{"start":{"line":643,"column":37},"end":{"line":645,"column":null}},"230":{"start":{"line":647,"column":38},"end":{"line":649,"column":null}},"231":{"start":{"line":652,"column":42},"end":{"line":654,"column":null}},"232":{"start":{"line":656,"column":43},"end":{"line":658,"column":null}},"233":{"start":{"line":661,"column":12},"end":{"line":681,"column":14}},"234":{"start":{"line":684,"column":8},"end":{"line":684,"column":35}},"235":{"start":{"line":697,"column":30},"end":{"line":698,"column":null}},"236":{"start":{"line":698,"column":45},"end":{"line":698,"column":73}},"237":{"start":{"line":700,"column":30},"end":{"line":701,"column":null}},"238":{"start":{"line":701,"column":45},"end":{"line":701,"column":63}},"239":{"start":{"line":703,"column":31},"end":{"line":704,"column":null}},"240":{"start":{"line":704,"column":45},"end":{"line":704,"column":64}},"241":{"start":{"line":707,"column":33},"end":{"line":709,"column":null}},"242":{"start":{"line":711,"column":33},"end":{"line":713,"column":null}},"243":{"start":{"line":715,"column":34},"end":{"line":717,"column":null}},"244":{"start":{"line":720,"column":38},"end":{"line":722,"column":null}},"245":{"start":{"line":724,"column":39},"end":{"line":726,"column":null}},"246":{"start":{"line":728,"column":8},"end":{"line":739,"column":10}},"247":{"start":{"line":753,"column":8},"end":{"line":755,"column":9}},"248":{"start":{"line":754,"column":12},"end":{"line":754,"column":24}},"249":{"start":{"line":756,"column":8},"end":{"line":756,"column":71}},"250":{"start":{"line":770,"column":8},"end":{"line":772,"column":9}},"251":{"start":{"line":771,"column":12},"end":{"line":771,"column":24}},"252":{"start":{"line":774,"column":8},"end":{"line":774,"column":57}},"253":{"start":{"line":783,"column":8},"end":{"line":783,"column":40}},"254":{"start":{"line":783,"column":28},"end":{"line":783,"column":40}},"255":{"start":{"line":784,"column":8},"end":{"line":784,"column":60}},"256":{"start":{"line":798,"column":53},"end":{"line":825,"column":14}},"257":{"start":{"line":800,"column":30},"end":{"line":800,"column":53}},"258":{"start":{"line":801,"column":30},"end":{"line":801,"column":53}},"259":{"start":{"line":802,"column":16},"end":{"line":802,"column":50}},"260":{"start":{"line":805,"column":16},"end":{"line":824,"column":18}},"261":{"start":{"line":827,"column":29},"end":{"line":829,"column":70}},"262":{"start":{"line":828,"column":24},"end":{"line":828,"column":39}},"263":{"start":{"line":829,"column":40},"end":{"line":829,"column":69}},"264":{"start":{"line":830,"column":28},"end":{"line":832,"column":70}},"265":{"start":{"line":831,"column":24},"end":{"line":831,"column":38}},"266":{"start":{"line":832,"column":40},"end":{"line":832,"column":69}},"267":{"start":{"line":835,"column":12},"end":{"line":835,"column":70}},"268":{"start":{"line":837,"column":12},"end":{"line":837,"column":70}},"269":{"start":{"line":839,"column":12},"end":{"line":839,"column":68}},"270":{"start":{"line":841,"column":12},"end":{"line":841,"column":68}},"271":{"start":{"line":843,"column":58},"end":{"line":868,"column":10}},"272":{"start":{"line":847,"column":26},"end":{"line":847,"column":77}},"273":{"start":{"line":853,"column":26},"end":{"line":853,"column":77}},"274":{"start":{"line":859,"column":26},"end":{"line":859,"column":75}},"275":{"start":{"line":865,"column":26},"end":{"line":865,"column":75}},"276":{"start":{"line":870,"column":57},"end":{"line":870,"column":59}},"277":{"start":{"line":872,"column":8},"end":{"line":884,"column":10}},"278":{"start":{"line":886,"column":8},"end":{"line":898,"column":10}},"279":{"start":{"line":900,"column":8},"end":{"line":912,"column":10}},"280":{"start":{"line":914,"column":8},"end":{"line":926,"column":10}},"281":{"start":{"line":928,"column":8},"end":{"line":928,"column":24}},"282":{"start":{"line":936,"column":30},"end":{"line":942,"column":5}},"283":{"start":{"line":937,"column":28},"end":{"line":938,"column":null}},"284":{"start":{"line":938,"column":37},"end":{"line":938,"column":70}},"285":{"start":{"line":940,"column":8},"end":{"line":940,"column":50}},"286":{"start":{"line":940,"column":38},"end":{"line":940,"column":50}},"287":{"start":{"line":941,"column":8},"end":{"line":941,"column":64}},"288":{"start":{"line":950,"column":18},"end":{"line":950,"column":28}},"289":{"start":{"line":951,"column":8},"end":{"line":953,"column":9}},"290":{"start":{"line":952,"column":12},"end":{"line":952,"column":34}},"291":{"start":{"line":954,"column":8},"end":{"line":954,"column":39}},"292":{"start":{"line":95,"column":13},"end":{"line":95,"column":38}},"293":{"start":{"line":95,"column":13},"end":{"line":956,"column":null}}},"fnMap":{"0":{"name":"(anonymous_5)","decl":{"start":{"line":98,"column":4},"end":{"line":98,"column":null}},"loc":{"start":{"line":128,"column":75},"end":{"line":129,"column":8}}},"1":{"name":"(anonymous_6)","decl":{"start":{"line":131,"column":4},"end":{"line":131,"column":9}},"loc":{"start":{"line":133,"column":26},"end":{"line":137,"column":5}}},"2":{"name":"(anonymous_7)","decl":{"start":{"line":143,"column":4},"end":{"line":143,"column":9}},"loc":{"start":{"line":143,"column":61},"end":{"line":155,"column":5}}},"3":{"name":"(anonymous_8)","decl":{"start":{"line":157,"column":4},"end":{"line":157,"column":9}},"loc":{"start":{"line":159,"column":26},"end":{"line":172,"column":5}}},"4":{"name":"(anonymous_9)","decl":{"start":{"line":179,"column":4},"end":{"line":179,"column":9}},"loc":{"start":{"line":181,"column":26},"end":{"line":207,"column":5}}},"5":{"name":"(anonymous_10)","decl":{"start":{"line":199,"column":12},"end":{"line":199,"column":13}},"loc":{"start":{"line":199,"column":24},"end":{"line":199,"column":53}}},"6":{"name":"(anonymous_11)","decl":{"start":{"line":209,"column":4},"end":{"line":209,"column":9}},"loc":{"start":{"line":211,"column":26},"end":{"line":223,"column":5}}},"7":{"name":"(anonymous_12)","decl":{"start":{"line":230,"column":4},"end":{"line":230,"column":9}},"loc":{"start":{"line":231,"column":23},"end":{"line":545,"column":5}}},"8":{"name":"(anonymous_13)","decl":{"start":{"line":319,"column":32},"end":{"line":319,"column":33}},"loc":{"start":{"line":319,"column":47},"end":{"line":325,"column":13}}},"9":{"name":"(anonymous_14)","decl":{"start":{"line":349,"column":28},"end":{"line":349,"column":29}},"loc":{"start":{"line":349,"column":39},"end":{"line":355,"column":13}}},"10":{"name":"(anonymous_15)","decl":{"start":{"line":362,"column":34},"end":{"line":362,"column":35}},"loc":{"start":{"line":362,"column":43},"end":{"line":372,"column":13}}},"11":{"name":"(anonymous_16)","decl":{"start":{"line":375,"column":16},"end":{"line":375,"column":17}},"loc":{"start":{"line":376,"column":20},"end":{"line":377,"column":54}}},"12":{"name":"(anonymous_17)","decl":{"start":{"line":382,"column":16},"end":{"line":382,"column":17}},"loc":{"start":{"line":382,"column":23},"end":{"line":382,"column":53}}},"13":{"name":"(anonymous_18)","decl":{"start":{"line":414,"column":30},"end":{"line":414,"column":31}},"loc":{"start":{"line":414,"column":44},"end":{"line":414,"column":70}}},"14":{"name":"(anonymous_19)","decl":{"start":{"line":418,"column":24},"end":{"line":418,"column":29}},"loc":{"start":{"line":418,"column":41},"end":{"line":426,"column":13}}},"15":{"name":"(anonymous_20)","decl":{"start":{"line":441,"column":50},"end":{"line":441,"column":51}},"loc":{"start":{"line":441,"column":57},"end":{"line":441,"column":62}}},"16":{"name":"(anonymous_21)","decl":{"start":{"line":442,"column":15},"end":{"line":442,"column":16}},"loc":{"start":{"line":442,"column":25},"end":{"line":442,"column":30}}},"17":{"name":"(anonymous_22)","decl":{"start":{"line":444,"column":55},"end":{"line":444,"column":56}},"loc":{"start":{"line":444,"column":62},"end":{"line":444,"column":77}}},"18":{"name":"(anonymous_23)","decl":{"start":{"line":445,"column":15},"end":{"line":445,"column":16}},"loc":{"start":{"line":445,"column":25},"end":{"line":445,"column":30}}},"19":{"name":"(anonymous_24)","decl":{"start":{"line":447,"column":55},"end":{"line":447,"column":56}},"loc":{"start":{"line":447,"column":65},"end":{"line":449,"column":9}}},"20":{"name":"(anonymous_25)","decl":{"start":{"line":451,"column":53},"end":{"line":451,"column":54}},"loc":{"start":{"line":451,"column":60},"end":{"line":451,"column":73}}},"21":{"name":"(anonymous_26)","decl":{"start":{"line":452,"column":15},"end":{"line":452,"column":16}},"loc":{"start":{"line":452,"column":25},"end":{"line":452,"column":30}}},"22":{"name":"(anonymous_27)","decl":{"start":{"line":453,"column":60},"end":{"line":453,"column":61}},"loc":{"start":{"line":453,"column":70},"end":{"line":453,"column":75}}},"23":{"name":"(anonymous_28)","decl":{"start":{"line":455,"column":12},"end":{"line":455,"column":13}},"loc":{"start":{"line":455,"column":22},"end":{"line":455,"column":27}}},"24":{"name":"(anonymous_29)","decl":{"start":{"line":458,"column":12},"end":{"line":458,"column":13}},"loc":{"start":{"line":458,"column":22},"end":{"line":458,"column":27}}},"25":{"name":"(anonymous_30)","decl":{"start":{"line":461,"column":12},"end":{"line":461,"column":13}},"loc":{"start":{"line":461,"column":22},"end":{"line":461,"column":27}}},"26":{"name":"(anonymous_31)","decl":{"start":{"line":465,"column":20},"end":{"line":465,"column":21}},"loc":{"start":{"line":465,"column":32},"end":{"line":465,"column":56}}},"27":{"name":"(anonymous_32)","decl":{"start":{"line":468,"column":20},"end":{"line":468,"column":21}},"loc":{"start":{"line":468,"column":32},"end":{"line":468,"column":55}}},"28":{"name":"(anonymous_33)","decl":{"start":{"line":471,"column":20},"end":{"line":471,"column":21}},"loc":{"start":{"line":471,"column":32},"end":{"line":471,"column":56}}},"29":{"name":"(anonymous_34)","decl":{"start":{"line":553,"column":12},"end":{"line":553,"column":28}},"loc":{"start":{"line":554,"column":56},"end":{"line":565,"column":5}}},"30":{"name":"(anonymous_35)","decl":{"start":{"line":557,"column":20},"end":{"line":557,"column":21}},"loc":{"start":{"line":557,"column":50},"end":{"line":557,"column":79}}},"31":{"name":"(anonymous_36)","decl":{"start":{"line":558,"column":17},"end":{"line":558,"column":18}},"loc":{"start":{"line":558,"column":25},"end":{"line":558,"column":66}}},"32":{"name":"(anonymous_37)","decl":{"start":{"line":561,"column":12},"end":{"line":561,"column":13}},"loc":{"start":{"line":561,"column":28},"end":{"line":561,"column":43}}},"33":{"name":"(anonymous_38)","decl":{"start":{"line":572,"column":12},"end":{"line":572,"column":24}},"loc":{"start":{"line":573,"column":50},"end":{"line":579,"column":5}}},"34":{"name":"(anonymous_39)","decl":{"start":{"line":588,"column":12},"end":{"line":588,"column":28}},"loc":{"start":{"line":590,"column":30},"end":{"line":599,"column":5}}},"35":{"name":"(anonymous_40)","decl":{"start":{"line":609,"column":12},"end":{"line":609,"column":41}},"loc":{"start":{"line":615,"column":42},"end":{"line":685,"column":5}}},"36":{"name":"(anonymous_41)","decl":{"start":{"line":621,"column":36},"end":{"line":621,"column":37}},"loc":{"start":{"line":621,"column":42},"end":{"line":627,"column":13}}},"37":{"name":"(anonymous_42)","decl":{"start":{"line":622,"column":43},"end":{"line":622,"column":44}},"loc":{"start":{"line":622,"column":49},"end":{"line":626,"column":17}}},"38":{"name":"(anonymous_43)","decl":{"start":{"line":630,"column":44},"end":{"line":630,"column":45}},"loc":{"start":{"line":630,"column":51},"end":{"line":630,"column":76}}},"39":{"name":"(anonymous_44)","decl":{"start":{"line":633,"column":44},"end":{"line":633,"column":45}},"loc":{"start":{"line":633,"column":51},"end":{"line":633,"column":66}}},"40":{"name":"(anonymous_45)","decl":{"start":{"line":636,"column":44},"end":{"line":636,"column":45}},"loc":{"start":{"line":636,"column":51},"end":{"line":636,"column":67}}},"41":{"name":"(anonymous_46)","decl":{"start":{"line":693,"column":12},"end":{"line":693,"column":44}},"loc":{"start":{"line":695,"column":25},"end":{"line":740,"column":5}}},"42":{"name":"(anonymous_47)","decl":{"start":{"line":698,"column":35},"end":{"line":698,"column":36}},"loc":{"start":{"line":698,"column":45},"end":{"line":698,"column":73}}},"43":{"name":"(anonymous_48)","decl":{"start":{"line":701,"column":35},"end":{"line":701,"column":36}},"loc":{"start":{"line":701,"column":45},"end":{"line":701,"column":63}}},"44":{"name":"(anonymous_49)","decl":{"start":{"line":704,"column":35},"end":{"line":704,"column":36}},"loc":{"start":{"line":704,"column":45},"end":{"line":704,"column":64}}},"45":{"name":"(anonymous_50)","decl":{"start":{"line":749,"column":12},"end":{"line":749,"column":31}},"loc":{"start":{"line":751,"column":25},"end":{"line":757,"column":5}}},"46":{"name":"(anonymous_51)","decl":{"start":{"line":766,"column":12},"end":{"line":766,"column":26}},"loc":{"start":{"line":768,"column":28},"end":{"line":775,"column":5}}},"47":{"name":"(anonymous_52)","decl":{"start":{"line":782,"column":12},"end":{"line":782,"column":27}},"loc":{"start":{"line":782,"column":49},"end":{"line":785,"column":5}}},"48":{"name":"(anonymous_53)","decl":{"start":{"line":794,"column":12},"end":{"line":794,"column":35}},"loc":{"start":{"line":796,"column":33},"end":{"line":929,"column":5}}},"49":{"name":"(anonymous_54)","decl":{"start":{"line":799,"column":18},"end":{"line":799,"column":19}},"loc":{"start":{"line":799,"column":27},"end":{"line":803,"column":13}}},"50":{"name":"(anonymous_55)","decl":{"start":{"line":804,"column":17},"end":{"line":804,"column":18}},"loc":{"start":{"line":804,"column":35},"end":{"line":825,"column":13}}},"51":{"name":"(anonymous_56)","decl":{"start":{"line":828,"column":17},"end":{"line":828,"column":18}},"loc":{"start":{"line":828,"column":24},"end":{"line":828,"column":39}}},"52":{"name":"(anonymous_57)","decl":{"start":{"line":829,"column":20},"end":{"line":829,"column":21}},"loc":{"start":{"line":829,"column":40},"end":{"line":829,"column":69}}},"53":{"name":"(anonymous_58)","decl":{"start":{"line":831,"column":17},"end":{"line":831,"column":18}},"loc":{"start":{"line":831,"column":24},"end":{"line":831,"column":38}}},"54":{"name":"(anonymous_59)","decl":{"start":{"line":832,"column":20},"end":{"line":832,"column":21}},"loc":{"start":{"line":832,"column":40},"end":{"line":832,"column":69}}},"55":{"name":"(anonymous_60)","decl":{"start":{"line":846,"column":36},"end":{"line":846,"column":37}},"loc":{"start":{"line":847,"column":26},"end":{"line":847,"column":77}}},"56":{"name":"(anonymous_61)","decl":{"start":{"line":852,"column":36},"end":{"line":852,"column":37}},"loc":{"start":{"line":853,"column":26},"end":{"line":853,"column":77}}},"57":{"name":"(anonymous_62)","decl":{"start":{"line":858,"column":36},"end":{"line":858,"column":37}},"loc":{"start":{"line":859,"column":26},"end":{"line":859,"column":75}}},"58":{"name":"(anonymous_63)","decl":{"start":{"line":864,"column":36},"end":{"line":864,"column":37}},"loc":{"start":{"line":865,"column":26},"end":{"line":865,"column":75}}},"59":{"name":"(anonymous_64)","decl":{"start":{"line":936,"column":30},"end":{"line":936,"column":31}},"loc":{"start":{"line":936,"column":70},"end":{"line":942,"column":5}}},"60":{"name":"(anonymous_65)","decl":{"start":{"line":938,"column":12},"end":{"line":938,"column":13}},"loc":{"start":{"line":938,"column":37},"end":{"line":938,"column":70}}},"61":{"name":"(anonymous_66)","decl":{"start":{"line":949,"column":12},"end":{"line":949,"column":42}},"loc":{"start":{"line":949,"column":61},"end":{"line":955,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":144,"column":8},"end":{"line":144,"column":27}},"type":"if","locations":[{"start":{"line":144,"column":8},"end":{"line":144,"column":27}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":147,"column":12},"end":{"line":148,"column":51}},"type":"binary-expr","locations":[{"start":{"line":147,"column":12},"end":{"line":147,"column":54}},{"start":{"line":148,"column":12},"end":{"line":148,"column":51}}]},"2":{"loc":{"start":{"line":150,"column":8},"end":{"line":154,"column":9}},"type":"if","locations":[{"start":{"line":150,"column":8},"end":{"line":154,"column":9}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":163,"column":8},"end":{"line":167,"column":9}},"type":"if","locations":[{"start":{"line":163,"column":8},"end":{"line":167,"column":9}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":183,"column":8},"end":{"line":183,"column":27}},"type":"if","locations":[{"start":{"line":183,"column":8},"end":{"line":183,"column":27}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":186,"column":12},"end":{"line":187,"column":51}},"type":"binary-expr","locations":[{"start":{"line":186,"column":12},"end":{"line":186,"column":54}},{"start":{"line":187,"column":12},"end":{"line":187,"column":51}}]},"6":{"loc":{"start":{"line":189,"column":8},"end":{"line":189,"column":32}},"type":"if","locations":[{"start":{"line":189,"column":8},"end":{"line":189,"column":32}},{"start":{},"end":{}}]},"7":{"loc":{"start":{"line":193,"column":8},"end":{"line":196,"column":9}},"type":"if","locations":[{"start":{"line":193,"column":8},"end":{"line":196,"column":9}},{"start":{},"end":{}}]},"8":{"loc":{"start":{"line":193,"column":12},"end":{"line":193,"column":44}},"type":"binary-expr","locations":[{"start":{"line":193,"column":12},"end":{"line":193,"column":20}},{"start":{"line":193,"column":24},"end":{"line":193,"column":44}}]},"9":{"loc":{"start":{"line":202,"column":8},"end":{"line":202,"column":38}},"type":"if","locations":[{"start":{"line":202,"column":8},"end":{"line":202,"column":38}},{"start":{},"end":{}}]},"10":{"loc":{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},"type":"if","locations":[{"start":{"line":214,"column":8},"end":{"line":218,"column":9}},{"start":{},"end":{}}]},"11":{"loc":{"start":{"line":236,"column":8},"end":{"line":238,"column":9}},"type":"if","locations":[{"start":{"line":236,"column":8},"end":{"line":238,"column":9}},{"start":{},"end":{}}]},"12":{"loc":{"start":{"line":241,"column":8},"end":{"line":244,"column":9}},"type":"if","locations":[{"start":{"line":241,"column":8},"end":{"line":244,"column":9}},{"start":{},"end":{}}]},"13":{"loc":{"start":{"line":247,"column":8},"end":{"line":251,"column":9}},"type":"if","locations":[{"start":{"line":247,"column":8},"end":{"line":251,"column":9}},{"start":{},"end":{}}]},"14":{"loc":{"start":{"line":247,"column":12},"end":{"line":247,"column":44}},"type":"binary-expr","locations":[{"start":{"line":247,"column":12},"end":{"line":247,"column":20}},{"start":{"line":247,"column":24},"end":{"line":247,"column":44}}]},"15":{"loc":{"start":{"line":265,"column":12},"end":{"line":268,"column":13}},"type":"if","locations":[{"start":{"line":265,"column":12},"end":{"line":268,"column":13}},{"start":{},"end":{}}]},"16":{"loc":{"start":{"line":273,"column":29},"end":{"line":275,"column":36}},"type":"cond-expr","locations":[{"start":{"line":274,"column":22},"end":{"line":274,"column":58}},{"start":{"line":275,"column":22},"end":{"line":275,"column":36}}]},"17":{"loc":{"start":{"line":276,"column":30},"end":{"line":278,"column":36}},"type":"cond-expr","locations":[{"start":{"line":277,"column":22},"end":{"line":277,"column":59}},{"start":{"line":278,"column":22},"end":{"line":278,"column":36}}]},"18":{"loc":{"start":{"line":303,"column":12},"end":{"line":313,"column":13}},"type":"if","locations":[{"start":{"line":303,"column":12},"end":{"line":313,"column":13}},{"start":{},"end":{}}]},"19":{"loc":{"start":{"line":306,"column":16},"end":{"line":308,"column":17}},"type":"if","locations":[{"start":{"line":306,"column":16},"end":{"line":308,"column":17}},{"start":{},"end":{}}]},"20":{"loc":{"start":{"line":310,"column":16},"end":{"line":312,"column":17}},"type":"if","locations":[{"start":{"line":310,"column":16},"end":{"line":312,"column":17}},{"start":{},"end":{}}]},"21":{"loc":{"start":{"line":322,"column":16},"end":{"line":324,"column":17}},"type":"if","locations":[{"start":{"line":322,"column":16},"end":{"line":324,"column":17}},{"start":{},"end":{}}]},"22":{"loc":{"start":{"line":328,"column":16},"end":{"line":333,"column":17}},"type":"if","locations":[{"start":{"line":328,"column":16},"end":{"line":333,"column":17}},{"start":{},"end":{}}]},"23":{"loc":{"start":{"line":330,"column":20},"end":{"line":332,"column":21}},"type":"if","locations":[{"start":{"line":330,"column":20},"end":{"line":332,"column":21}},{"start":{},"end":{}}]},"24":{"loc":{"start":{"line":337,"column":16},"end":{"line":342,"column":17}},"type":"if","locations":[{"start":{"line":337,"column":16},"end":{"line":342,"column":17}},{"start":{},"end":{}}]},"25":{"loc":{"start":{"line":339,"column":20},"end":{"line":341,"column":21}},"type":"if","locations":[{"start":{"line":339,"column":20},"end":{"line":341,"column":21}},{"start":{},"end":{}}]},"26":{"loc":{"start":{"line":352,"column":16},"end":{"line":354,"column":17}},"type":"if","locations":[{"start":{"line":352,"column":16},"end":{"line":354,"column":17}},{"start":{},"end":{}}]},"27":{"loc":{"start":{"line":363,"column":16},"end":{"line":371,"column":17}},"type":"if","locations":[{"start":{"line":363,"column":16},"end":{"line":371,"column":17}},{"start":{},"end":{}}]},"28":{"loc":{"start":{"line":380,"column":16},"end":{"line":380,"column":68}},"type":"binary-expr","locations":[{"start":{"line":380,"column":16},"end":{"line":380,"column":34}},{"start":{"line":380,"column":38},"end":{"line":380,"column":68}}]},"29":{"loc":{"start":{"line":384,"column":12},"end":{"line":388,"column":13}},"type":"if","locations":[{"start":{"line":384,"column":12},"end":{"line":388,"column":13}},{"start":{},"end":{}}]},"30":{"loc":{"start":{"line":385,"column":46},"end":{"line":387,"column":36}},"type":"cond-expr","locations":[{"start":{"line":386,"column":22},"end":{"line":386,"column":38}},{"start":{"line":387,"column":22},"end":{"line":387,"column":36}}]},"31":{"loc":{"start":{"line":391,"column":16},"end":{"line":396,"column":17}},"type":"if","locations":[{"start":{"line":391,"column":16},"end":{"line":396,"column":17}},{"start":{},"end":{}}]},"32":{"loc":{"start":{"line":393,"column":20},"end":{"line":395,"column":21}},"type":"if","locations":[{"start":{"line":393,"column":20},"end":{"line":395,"column":21}},{"start":{},"end":{}}]},"33":{"loc":{"start":{"line":398,"column":16},"end":{"line":400,"column":17}},"type":"if","locations":[{"start":{"line":398,"column":16},"end":{"line":400,"column":17}},{"start":{},"end":{}}]},"34":{"loc":{"start":{"line":398,"column":20},"end":{"line":398,"column":39}},"type":"binary-expr","locations":[{"start":{"line":398,"column":20},"end":{"line":398,"column":21}},{"start":{"line":398,"column":25},"end":{"line":398,"column":39}}]},"35":{"loc":{"start":{"line":404,"column":16},"end":{"line":410,"column":17}},"type":"if","locations":[{"start":{"line":404,"column":16},"end":{"line":410,"column":17}},{"start":{},"end":{}}]},"36":{"loc":{"start":{"line":407,"column":20},"end":{"line":409,"column":21}},"type":"if","locations":[{"start":{"line":407,"column":20},"end":{"line":409,"column":21}},{"start":{},"end":{}}]},"37":{"loc":{"start":{"line":534,"column":8},"end":{"line":538,"column":9}},"type":"if","locations":[{"start":{"line":534,"column":8},"end":{"line":538,"column":9}},{"start":{},"end":{}}]},"38":{"loc":{"start":{"line":557,"column":50},"end":{"line":557,"column":79}},"type":"binary-expr","locations":[{"start":{"line":557,"column":50},"end":{"line":557,"column":60}},{"start":{"line":557,"column":64},"end":{"line":557,"column":79}}]},"39":{"loc":{"start":{"line":558,"column":25},"end":{"line":558,"column":66}},"type":"cond-expr","locations":[{"start":{"line":558,"column":48},"end":{"line":558,"column":49}},{"start":{"line":558,"column":52},"end":{"line":558,"column":66}}]},"40":{"loc":{"start":{"line":559,"column":8},"end":{"line":559,"column":51}},"type":"if","locations":[{"start":{"line":559,"column":8},"end":{"line":559,"column":51}},{"start":{},"end":{}}]},"41":{"loc":{"start":{"line":575,"column":8},"end":{"line":575,"column":63}},"type":"if","locations":[{"start":{"line":575,"column":8},"end":{"line":575,"column":63}},{"start":{},"end":{}}]},"42":{"loc":{"start":{"line":575,"column":12},"end":{"line":575,"column":49}},"type":"binary-expr","locations":[{"start":{"line":575,"column":12},"end":{"line":575,"column":26}},{"start":{"line":575,"column":30},"end":{"line":575,"column":49}}]},"43":{"loc":{"start":{"line":577,"column":12},"end":{"line":577,"column":65}},"type":"cond-expr","locations":[{"start":{"line":577,"column":39},"end":{"line":577,"column":44}},{"start":{"line":577,"column":47},"end":{"line":577,"column":65}}]},"44":{"loc":{"start":{"line":592,"column":8},"end":{"line":594,"column":9}},"type":"if","locations":[{"start":{"line":592,"column":8},"end":{"line":594,"column":9}},{"start":{},"end":{}}]},"45":{"loc":{"start":{"line":598,"column":15},"end":{"line":598,"column":59}},"type":"binary-expr","locations":[{"start":{"line":598,"column":15},"end":{"line":598,"column":41}},{"start":{"line":598,"column":45},"end":{"line":598,"column":59}}]},"46":{"loc":{"start":{"line":623,"column":20},"end":{"line":625,"column":21}},"type":"if","locations":[{"start":{"line":623,"column":20},"end":{"line":625,"column":21}},{"start":{},"end":{}}]},"47":{"loc":{"start":{"line":667,"column":24},"end":{"line":667,"column":64}},"type":"binary-expr","locations":[{"start":{"line":667,"column":24},"end":{"line":667,"column":56}},{"start":{"line":667,"column":60},"end":{"line":667,"column":64}}]},"48":{"loc":{"start":{"line":668,"column":35},"end":{"line":668,"column":75}},"type":"binary-expr","locations":[{"start":{"line":668,"column":35},"end":{"line":668,"column":67}},{"start":{"line":668,"column":71},"end":{"line":668,"column":75}}]},"49":{"loc":{"start":{"line":669,"column":36},"end":{"line":669,"column":77}},"type":"binary-expr","locations":[{"start":{"line":669,"column":36},"end":{"line":669,"column":69}},{"start":{"line":669,"column":73},"end":{"line":669,"column":77}}]},"50":{"loc":{"start":{"line":671,"column":24},"end":{"line":671,"column":67}},"type":"binary-expr","locations":[{"start":{"line":671,"column":24},"end":{"line":671,"column":59}},{"start":{"line":671,"column":63},"end":{"line":671,"column":67}}]},"51":{"loc":{"start":{"line":673,"column":24},"end":{"line":673,"column":67}},"type":"binary-expr","locations":[{"start":{"line":673,"column":24},"end":{"line":673,"column":59}},{"start":{"line":673,"column":63},"end":{"line":673,"column":67}}]},"52":{"loc":{"start":{"line":675,"column":24},"end":{"line":675,"column":68}},"type":"binary-expr","locations":[{"start":{"line":675,"column":24},"end":{"line":675,"column":60}},{"start":{"line":675,"column":64},"end":{"line":675,"column":68}}]},"53":{"loc":{"start":{"line":677,"column":24},"end":{"line":677,"column":72}},"type":"binary-expr","locations":[{"start":{"line":677,"column":24},"end":{"line":677,"column":64}},{"start":{"line":677,"column":68},"end":{"line":677,"column":72}}]},"54":{"loc":{"start":{"line":679,"column":24},"end":{"line":679,"column":73}},"type":"binary-expr","locations":[{"start":{"line":679,"column":24},"end":{"line":679,"column":65}},{"start":{"line":679,"column":69},"end":{"line":679,"column":73}}]},"55":{"loc":{"start":{"line":753,"column":8},"end":{"line":755,"column":9}},"type":"if","locations":[{"start":{"line":753,"column":8},"end":{"line":755,"column":9}},{"start":{},"end":{}}]},"56":{"loc":{"start":{"line":753,"column":12},"end":{"line":753,"column":47}},"type":"binary-expr","locations":[{"start":{"line":753,"column":12},"end":{"line":753,"column":26}},{"start":{"line":753,"column":30},"end":{"line":753,"column":47}}]},"57":{"loc":{"start":{"line":770,"column":8},"end":{"line":772,"column":9}},"type":"if","locations":[{"start":{"line":770,"column":8},"end":{"line":772,"column":9}},{"start":{},"end":{}}]},"58":{"loc":{"start":{"line":770,"column":12},"end":{"line":770,"column":48}},"type":"binary-expr","locations":[{"start":{"line":770,"column":12},"end":{"line":770,"column":31}},{"start":{"line":770,"column":35},"end":{"line":770,"column":48}}]},"59":{"loc":{"start":{"line":783,"column":8},"end":{"line":783,"column":40}},"type":"if","locations":[{"start":{"line":783,"column":8},"end":{"line":783,"column":40}},{"start":{},"end":{}}]},"60":{"loc":{"start":{"line":800,"column":30},"end":{"line":800,"column":53}},"type":"binary-expr","locations":[{"start":{"line":800,"column":30},"end":{"line":800,"column":47}},{"start":{"line":800,"column":51},"end":{"line":800,"column":53}}]},"61":{"loc":{"start":{"line":801,"column":30},"end":{"line":801,"column":53}},"type":"binary-expr","locations":[{"start":{"line":801,"column":30},"end":{"line":801,"column":47}},{"start":{"line":801,"column":51},"end":{"line":801,"column":53}}]},"62":{"loc":{"start":{"line":829,"column":40},"end":{"line":829,"column":69}},"type":"binary-expr","locations":[{"start":{"line":829,"column":40},"end":{"line":829,"column":55}},{"start":{"line":829,"column":59},"end":{"line":829,"column":69}}]},"63":{"loc":{"start":{"line":832,"column":40},"end":{"line":832,"column":69}},"type":"binary-expr","locations":[{"start":{"line":832,"column":40},"end":{"line":832,"column":55}},{"start":{"line":832,"column":59},"end":{"line":832,"column":69}}]},"64":{"loc":{"start":{"line":835,"column":12},"end":{"line":835,"column":70}},"type":"cond-expr","locations":[{"start":{"line":835,"column":38},"end":{"line":835,"column":63}},{"start":{"line":835,"column":66},"end":{"line":835,"column":70}}]},"65":{"loc":{"start":{"line":837,"column":12},"end":{"line":837,"column":70}},"type":"cond-expr","locations":[{"start":{"line":837,"column":38},"end":{"line":837,"column":63}},{"start":{"line":837,"column":66},"end":{"line":837,"column":70}}]},"66":{"loc":{"start":{"line":839,"column":12},"end":{"line":839,"column":68}},"type":"cond-expr","locations":[{"start":{"line":839,"column":37},"end":{"line":839,"column":61}},{"start":{"line":839,"column":64},"end":{"line":839,"column":68}}]},"67":{"loc":{"start":{"line":841,"column":12},"end":{"line":841,"column":68}},"type":"cond-expr","locations":[{"start":{"line":841,"column":37},"end":{"line":841,"column":61}},{"start":{"line":841,"column":64},"end":{"line":841,"column":68}}]},"68":{"loc":{"start":{"line":845,"column":16},"end":{"line":849,"column":31}},"type":"cond-expr","locations":[{"start":{"line":846,"column":22},"end":{"line":847,"column":null}},{"start":{"line":849,"column":22},"end":{"line":849,"column":31}}]},"69":{"loc":{"start":{"line":847,"column":38},"end":{"line":847,"column":58}},"type":"binary-expr","locations":[{"start":{"line":847,"column":38},"end":{"line":847,"column":53}},{"start":{"line":847,"column":57},"end":{"line":847,"column":58}}]},"70":{"loc":{"start":{"line":851,"column":16},"end":{"line":855,"column":31}},"type":"cond-expr","locations":[{"start":{"line":852,"column":22},"end":{"line":853,"column":null}},{"start":{"line":855,"column":22},"end":{"line":855,"column":31}}]},"71":{"loc":{"start":{"line":853,"column":38},"end":{"line":853,"column":58}},"type":"binary-expr","locations":[{"start":{"line":853,"column":38},"end":{"line":853,"column":53}},{"start":{"line":853,"column":57},"end":{"line":853,"column":58}}]},"72":{"loc":{"start":{"line":857,"column":16},"end":{"line":861,"column":31}},"type":"cond-expr","locations":[{"start":{"line":858,"column":22},"end":{"line":859,"column":null}},{"start":{"line":861,"column":22},"end":{"line":861,"column":31}}]},"73":{"loc":{"start":{"line":859,"column":38},"end":{"line":859,"column":57}},"type":"binary-expr","locations":[{"start":{"line":859,"column":38},"end":{"line":859,"column":52}},{"start":{"line":859,"column":56},"end":{"line":859,"column":57}}]},"74":{"loc":{"start":{"line":863,"column":16},"end":{"line":867,"column":31}},"type":"cond-expr","locations":[{"start":{"line":864,"column":22},"end":{"line":865,"column":null}},{"start":{"line":867,"column":22},"end":{"line":867,"column":31}}]},"75":{"loc":{"start":{"line":865,"column":38},"end":{"line":865,"column":57}},"type":"binary-expr","locations":[{"start":{"line":865,"column":38},"end":{"line":865,"column":52}},{"start":{"line":865,"column":56},"end":{"line":865,"column":57}}]},"76":{"loc":{"start":{"line":876,"column":30},"end":{"line":876,"column":76}},"type":"binary-expr","locations":[{"start":{"line":876,"column":30},"end":{"line":876,"column":70}},{"start":{"line":876,"column":74},"end":{"line":876,"column":76}}]},"77":{"loc":{"start":{"line":878,"column":20},"end":{"line":878,"column":69}},"type":"binary-expr","locations":[{"start":{"line":878,"column":20},"end":{"line":878,"column":63}},{"start":{"line":878,"column":67},"end":{"line":878,"column":69}}]},"78":{"loc":{"start":{"line":890,"column":30},"end":{"line":890,"column":75}},"type":"binary-expr","locations":[{"start":{"line":890,"column":30},"end":{"line":890,"column":69}},{"start":{"line":890,"column":73},"end":{"line":890,"column":75}}]},"79":{"loc":{"start":{"line":892,"column":20},"end":{"line":892,"column":68}},"type":"binary-expr","locations":[{"start":{"line":892,"column":20},"end":{"line":892,"column":62}},{"start":{"line":892,"column":66},"end":{"line":892,"column":68}}]},"80":{"loc":{"start":{"line":904,"column":30},"end":{"line":904,"column":75}},"type":"binary-expr","locations":[{"start":{"line":904,"column":30},"end":{"line":904,"column":69}},{"start":{"line":904,"column":73},"end":{"line":904,"column":75}}]},"81":{"loc":{"start":{"line":906,"column":20},"end":{"line":906,"column":68}},"type":"binary-expr","locations":[{"start":{"line":906,"column":20},"end":{"line":906,"column":62}},{"start":{"line":906,"column":66},"end":{"line":906,"column":68}}]},"82":{"loc":{"start":{"line":918,"column":30},"end":{"line":918,"column":74}},"type":"binary-expr","locations":[{"start":{"line":918,"column":30},"end":{"line":918,"column":68}},{"start":{"line":918,"column":72},"end":{"line":918,"column":74}}]},"83":{"loc":{"start":{"line":920,"column":20},"end":{"line":920,"column":67}},"type":"binary-expr","locations":[{"start":{"line":920,"column":20},"end":{"line":920,"column":61}},{"start":{"line":920,"column":65},"end":{"line":920,"column":67}}]},"84":{"loc":{"start":{"line":938,"column":37},"end":{"line":938,"column":70}},"type":"binary-expr","locations":[{"start":{"line":938,"column":37},"end":{"line":938,"column":49}},{"start":{"line":938,"column":53},"end":{"line":938,"column":70}}]},"85":{"loc":{"start":{"line":940,"column":8},"end":{"line":940,"column":50}},"type":"if","locations":[{"start":{"line":940,"column":8},"end":{"line":940,"column":50}},{"start":{},"end":{}}]},"86":{"loc":{"start":{"line":100,"column":43},"end":{"line":100,"column":null}},"type":"cond-expr","locations":[{"start":{"line":100,"column":72},"end":{"line":100,"column":null}},{"start":{"line":100,"column":72},"end":{"line":100,"column":null}}]},"87":{"loc":{"start":{"line":100,"column":43},"end":{"line":100,"column":72}},"type":"binary-expr","locations":[{"start":{"line":100,"column":43},"end":{"line":100,"column":null}},{"start":{"line":100,"column":43},"end":{"line":100,"column":72}}]},"88":{"loc":{"start":{"line":100,"column":72},"end":{"line":102,"column":null}},"type":"cond-expr","locations":[{"start":{"line":102,"column":87},"end":{"line":102,"column":null}},{"start":{"line":102,"column":87},"end":{"line":102,"column":null}}]},"89":{"loc":{"start":{"line":100,"column":72},"end":{"line":102,"column":87}},"type":"binary-expr","locations":[{"start":{"line":100,"column":72},"end":{"line":102,"column":null}},{"start":{"line":102,"column":49},"end":{"line":102,"column":87}}]},"90":{"loc":{"start":{"line":102,"column":87},"end":{"line":104,"column":null}},"type":"cond-expr","locations":[{"start":{"line":104,"column":62},"end":{"line":104,"column":null}},{"start":{"line":104,"column":62},"end":{"line":104,"column":null}}]},"91":{"loc":{"start":{"line":102,"column":87},"end":{"line":104,"column":62}},"type":"binary-expr","locations":[{"start":{"line":102,"column":87},"end":{"line":104,"column":null}},{"start":{"line":104,"column":38},"end":{"line":104,"column":62}}]},"92":{"loc":{"start":{"line":104,"column":62},"end":{"line":106,"column":null}},"type":"cond-expr","locations":[{"start":{"line":106,"column":58},"end":{"line":106,"column":null}},{"start":{"line":106,"column":58},"end":{"line":106,"column":null}}]},"93":{"loc":{"start":{"line":104,"column":62},"end":{"line":106,"column":58}},"type":"binary-expr","locations":[{"start":{"line":104,"column":62},"end":{"line":106,"column":null}},{"start":{"line":106,"column":36},"end":{"line":106,"column":58}}]},"94":{"loc":{"start":{"line":106,"column":58},"end":{"line":108,"column":null}},"type":"cond-expr","locations":[{"start":{"line":108,"column":54},"end":{"line":108,"column":null}},{"start":{"line":108,"column":54},"end":{"line":108,"column":null}}]},"95":{"loc":{"start":{"line":106,"column":58},"end":{"line":108,"column":54}},"type":"binary-expr","locations":[{"start":{"line":106,"column":58},"end":{"line":108,"column":null}},{"start":{"line":108,"column":34},"end":{"line":108,"column":54}}]},"96":{"loc":{"start":{"line":108,"column":54},"end":{"line":110,"column":null}},"type":"cond-expr","locations":[{"start":{"line":110,"column":54},"end":{"line":110,"column":null}},{"start":{"line":110,"column":54},"end":{"line":110,"column":null}}]},"97":{"loc":{"start":{"line":108,"column":54},"end":{"line":110,"column":54}},"type":"binary-expr","locations":[{"start":{"line":108,"column":54},"end":{"line":110,"column":null}},{"start":{"line":110,"column":34},"end":{"line":110,"column":54}}]},"98":{"loc":{"start":{"line":110,"column":54},"end":{"line":112,"column":null}},"type":"cond-expr","locations":[{"start":{"line":112,"column":56},"end":{"line":112,"column":null}},{"start":{"line":112,"column":56},"end":{"line":112,"column":null}}]},"99":{"loc":{"start":{"line":110,"column":54},"end":{"line":112,"column":56}},"type":"binary-expr","locations":[{"start":{"line":110,"column":54},"end":{"line":112,"column":null}},{"start":{"line":112,"column":37},"end":{"line":112,"column":56}}]},"100":{"loc":{"start":{"line":112,"column":56},"end":{"line":114,"column":null}},"type":"cond-expr","locations":[{"start":{"line":114,"column":54},"end":{"line":114,"column":null}},{"start":{"line":114,"column":54},"end":{"line":114,"column":null}}]},"101":{"loc":{"start":{"line":112,"column":56},"end":{"line":114,"column":54}},"type":"binary-expr","locations":[{"start":{"line":112,"column":56},"end":{"line":114,"column":null}},{"start":{"line":114,"column":34},"end":{"line":114,"column":54}}]},"102":{"loc":{"start":{"line":114,"column":54},"end":{"line":116,"column":null}},"type":"cond-expr","locations":[{"start":{"line":116,"column":50},"end":{"line":116,"column":null}},{"start":{"line":116,"column":50},"end":{"line":116,"column":null}}]},"103":{"loc":{"start":{"line":114,"column":54},"end":{"line":116,"column":50}},"type":"binary-expr","locations":[{"start":{"line":114,"column":54},"end":{"line":116,"column":null}},{"start":{"line":116,"column":32},"end":{"line":116,"column":50}}]},"104":{"loc":{"start":{"line":116,"column":50},"end":{"line":118,"column":null}},"type":"cond-expr","locations":[{"start":{"line":118,"column":50},"end":{"line":118,"column":null}},{"start":{"line":118,"column":50},"end":{"line":118,"column":null}}]},"105":{"loc":{"start":{"line":116,"column":50},"end":{"line":118,"column":50}},"type":"binary-expr","locations":[{"start":{"line":116,"column":50},"end":{"line":118,"column":null}},{"start":{"line":118,"column":32},"end":{"line":118,"column":50}}]},"106":{"loc":{"start":{"line":118,"column":50},"end":{"line":120,"column":null}},"type":"cond-expr","locations":[{"start":{"line":120,"column":58},"end":{"line":120,"column":null}},{"start":{"line":120,"column":58},"end":{"line":120,"column":null}}]},"107":{"loc":{"start":{"line":118,"column":50},"end":{"line":120,"column":58}},"type":"binary-expr","locations":[{"start":{"line":118,"column":50},"end":{"line":120,"column":null}},{"start":{"line":120,"column":36},"end":{"line":120,"column":58}}]},"108":{"loc":{"start":{"line":120,"column":58},"end":{"line":122,"column":null}},"type":"cond-expr","locations":[{"start":{"line":122,"column":62},"end":{"line":122,"column":null}},{"start":{"line":122,"column":62},"end":{"line":122,"column":null}}]},"109":{"loc":{"start":{"line":120,"column":58},"end":{"line":122,"column":62}},"type":"binary-expr","locations":[{"start":{"line":120,"column":58},"end":{"line":122,"column":null}},{"start":{"line":122,"column":38},"end":{"line":122,"column":62}}]},"110":{"loc":{"start":{"line":122,"column":62},"end":{"line":124,"column":null}},"type":"cond-expr","locations":[{"start":{"line":124,"column":58},"end":{"line":124,"column":null}},{"start":{"line":124,"column":58},"end":{"line":124,"column":null}}]},"111":{"loc":{"start":{"line":122,"column":62},"end":{"line":124,"column":58}},"type":"binary-expr","locations":[{"start":{"line":122,"column":62},"end":{"line":124,"column":null}},{"start":{"line":124,"column":36},"end":{"line":124,"column":58}}]},"112":{"loc":{"start":{"line":124,"column":58},"end":{"line":126,"column":null}},"type":"cond-expr","locations":[{"start":{"line":126,"column":75},"end":{"line":126,"column":null}},{"start":{"line":126,"column":75},"end":{"line":126,"column":null}}]},"113":{"loc":{"start":{"line":124,"column":58},"end":{"line":126,"column":75}},"type":"binary-expr","locations":[{"start":{"line":124,"column":58},"end":{"line":126,"column":null}},{"start":{"line":126,"column":46},"end":{"line":126,"column":75}}]},"114":{"loc":{"start":{"line":126,"column":75},"end":{"line":128,"column":null}},"type":"cond-expr","locations":[{"start":{"line":128,"column":75},"end":{"line":128,"column":null}},{"start":{"line":128,"column":75},"end":{"line":128,"column":null}}]},"115":{"loc":{"start":{"line":126,"column":75},"end":{"line":128,"column":75}},"type":"binary-expr","locations":[{"start":{"line":126,"column":75},"end":{"line":128,"column":null}},{"start":{"line":128,"column":39},"end":{"line":128,"column":75}}]}},"s":{"0":5,"1":5,"2":5,"3":5,"4":5,"5":5,"6":5,"7":5,"8":5,"9":5,"10":5,"11":5,"12":5,"13":5,"14":5,"15":5,"16":5,"17":5,"18":5,"19":5,"20":5,"21":5,"22":16,"23":16,"24":16,"25":16,"26":16,"27":16,"28":16,"29":16,"30":16,"31":16,"32":16,"33":16,"34":16,"35":16,"36":16,"37":16,"38":3,"39":2,"40":3,"41":1,"42":2,"43":2,"44":1,"45":5,"46":5,"47":1,"48":4,"49":2,"50":5,"51":1,"52":4,"53":4,"54":1,"55":3,"56":3,"57":1,"58":2,"59":2,"60":2,"61":1,"62":1,"63":2,"64":2,"65":1,"66":1,"67":1,"68":6,"69":6,"70":6,"71":1,"72":5,"73":5,"74":1,"75":1,"76":4,"77":4,"78":1,"79":3,"80":3,"81":3,"82":3,"83":3,"84":3,"85":2,"86":2,"87":3,"88":3,"89":3,"90":3,"91":3,"92":3,"93":3,"94":3,"95":3,"96":3,"97":3,"98":2,"99":2,"100":2,"101":2,"102":2,"103":2,"104":2,"105":2,"106":1,"107":2,"108":1,"109":2,"110":2,"111":2,"112":2,"113":2,"114":2,"115":2,"116":2,"117":2,"118":2,"119":2,"120":1,"121":1,"122":1,"123":1,"124":2,"125":2,"126":2,"127":2,"128":2,"129":2,"130":2,"131":3,"132":3,"133":2,"134":1,"135":2,"136":2,"137":2,"138":2,"139":2,"140":2,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":2,"149":1,"150":1,"151":1,"152":1,"153":2,"154":2,"155":1,"156":3,"157":3,"158":3,"159":3,"160":3,"161":3,"162":2,"163":0,"164":3,"165":2,"166":1,"167":3,"168":3,"169":9,"170":3,"171":1,"172":0,"173":3,"174":1,"175":3,"176":0,"177":3,"178":0,"179":3,"180":0,"181":3,"182":3,"183":3,"184":3,"185":3,"186":3,"187":3,"188":3,"189":3,"190":3,"191":3,"192":3,"193":3,"194":3,"195":3,"196":1,"197":2,"198":2,"199":12,"200":6,"201":6,"202":12,"203":6,"204":6,"205":6,"206":6,"207":26,"208":0,"209":26,"210":26,"211":0,"212":0,"213":0,"214":0,"215":3,"216":3,"217":1,"218":1,"219":1,"220":1,"221":1,"222":1,"223":1,"224":1,"225":1,"226":1,"227":1,"228":1,"229":1,"230":1,"231":1,"232":1,"233":1,"234":3,"235":3,"236":1,"237":3,"238":1,"239":3,"240":1,"241":3,"242":3,"243":3,"244":3,"245":3,"246":3,"247":12,"248":6,"249":6,"250":8,"251":4,"252":4,"253":24,"254":16,"255":8,"256":3,"257":0,"258":0,"259":0,"260":1,"261":3,"262":1,"263":1,"264":3,"265":1,"266":1,"267":3,"268":3,"269":3,"270":3,"271":3,"272":1,"273":1,"274":1,"275":1,"276":3,"277":3,"278":3,"279":3,"280":3,"281":3,"282":16,"283":3,"284":6,"285":3,"286":0,"287":3,"288":3,"289":3,"290":6,"291":3,"292":5,"293":5},"f":{"0":16,"1":3,"2":3,"3":5,"4":5,"5":2,"6":2,"7":6,"8":2,"9":2,"10":3,"11":1,"12":2,"13":1,"14":3,"15":2,"16":0,"17":2,"18":1,"19":9,"20":1,"21":0,"22":1,"23":0,"24":0,"25":0,"26":3,"27":3,"28":3,"29":12,"30":6,"31":6,"32":6,"33":26,"34":0,"35":3,"36":1,"37":1,"38":1,"39":1,"40":1,"41":3,"42":1,"43":1,"44":1,"45":12,"46":8,"47":24,"48":3,"49":0,"50":1,"51":1,"52":1,"53":1,"54":1,"55":1,"56":1,"57":1,"58":1,"59":3,"60":6,"61":3},"b":{"0":[1,2],"1":[2,2],"2":[1,1],"3":[1,4],"4":[1,4],"5":[4,4],"6":[1,3],"7":[1,2],"8":[3,3],"9":[1,1],"10":[1,1],"11":[1,5],"12":[1,4],"13":[1,3],"14":[4,4],"15":[2,1],"16":[1,2],"17":[1,2],"18":[2,0],"19":[1,1],"20":[1,1],"21":[2,0],"22":[2,0],"23":[2,0],"24":[1,0],"25":[1,0],"26":[2,0],"27":[3,0],"28":[2,1],"29":[2,0],"30":[1,1],"31":[1,0],"32":[1,0],"33":[1,0],"34":[1,1],"35":[1,0],"36":[1,0],"37":[1,2],"38":[6,6],"39":[6,0],"40":[6,6],"41":[0,26],"42":[26,26],"43":[26,0],"44":[0,0],"45":[0,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[6,6],"56":[12,6],"57":[4,4],"58":[8,4],"59":[16,8],"60":[0,0],"61":[0,0],"62":[1,1],"63":[1,1],"64":[1,2],"65":[1,2],"66":[1,2],"67":[1,2],"68":[1,2],"69":[1,0],"70":[1,2],"71":[1,0],"72":[1,2],"73":[1,0],"74":[1,2],"75":[1,0],"76":[3,2],"77":[3,2],"78":[3,2],"79":[3,2],"80":[3,2],"81":[3,2],"82":[3,2],"83":[3,2],"84":[6,6],"85":[0,3],"86":[0,5],"87":[5,0],"88":[0,5],"89":[5,0],"90":[0,5],"91":[5,0],"92":[0,5],"93":[5,0],"94":[0,5],"95":[5,0],"96":[0,5],"97":[5,0],"98":[0,5],"99":[5,0],"100":[0,5],"101":[5,0],"102":[0,5],"103":[5,0],"104":[0,5],"105":[5,0],"106":[0,5],"107":[5,0],"108":[0,5],"109":[5,0],"110":[0,5],"111":[5,0],"112":[0,5],"113":[5,0],"114":[0,5],"115":[5,0]}} +,"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/text-answer.service.ts": {"path":"/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/src/contexts/reporting/application/services/text-answer.service.ts","statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":null}},"1":{"start":{"line":6,"column":0},"end":{"line":6,"column":72}},"2":{"start":{"line":8,"column":0},"end":{"line":8,"column":null}},"3":{"start":{"line":12,"column":0},"end":{"line":12,"column":null}},"4":{"start":{"line":19,"column":7},"end":{"line":79,"column":null}},"5":{"start":{"line":22,"column":25},"end":{"line":22,"column":34}},"6":{"start":{"line":24,"column":25},"end":{"line":24,"column":36}},"7":{"start":{"line":31,"column":8},"end":{"line":31,"column":51}},"8":{"start":{"line":33,"column":37},"end":{"line":39,"column":10}},"9":{"start":{"line":41,"column":28},"end":{"line":41,"column":75}},"10":{"start":{"line":42,"column":8},"end":{"line":44,"column":11}},"11":{"start":{"line":43,"column":12},"end":{"line":43,"column":59}},"12":{"start":{"line":46,"column":8},"end":{"line":58,"column":15}},"13":{"start":{"line":48,"column":28},"end":{"line":48,"column":78}},"14":{"start":{"line":51,"column":33},"end":{"line":51,"column":67}},"15":{"start":{"line":52,"column":16},"end":{"line":57,"column":18}},"16":{"start":{"line":67,"column":8},"end":{"line":67,"column":27}},"17":{"start":{"line":67,"column":20},"end":{"line":67,"column":27}},"18":{"start":{"line":70,"column":12},"end":{"line":71,"column":51}},"19":{"start":{"line":73,"column":8},"end":{"line":73,"column":32}},"20":{"start":{"line":73,"column":25},"end":{"line":73,"column":32}},"21":{"start":{"line":75,"column":8},"end":{"line":77,"column":10}},"22":{"start":{"line":19,"column":13},"end":{"line":19,"column":30}},"23":{"start":{"line":19,"column":13},"end":{"line":79,"column":null}}},"fnMap":{"0":{"name":"(anonymous_4)","decl":{"start":{"line":20,"column":4},"end":{"line":20,"column":null}},"loc":{"start":{"line":24,"column":72},"end":{"line":25,"column":8}}},"1":{"name":"(anonymous_5)","decl":{"start":{"line":27,"column":4},"end":{"line":27,"column":9}},"loc":{"start":{"line":29,"column":26},"end":{"line":59,"column":5}}},"2":{"name":"(anonymous_6)","decl":{"start":{"line":42,"column":26},"end":{"line":42,"column":27}},"loc":{"start":{"line":42,"column":39},"end":{"line":44,"column":9}}},"3":{"name":"(anonymous_7)","decl":{"start":{"line":48,"column":16},"end":{"line":48,"column":17}},"loc":{"start":{"line":48,"column":28},"end":{"line":48,"column":78}}},"4":{"name":"(anonymous_8)","decl":{"start":{"line":50,"column":17},"end":{"line":50,"column":18}},"loc":{"start":{"line":50,"column":28},"end":{"line":58,"column":13}}},"5":{"name":"(anonymous_9)","decl":{"start":{"line":66,"column":12},"end":{"line":66,"column":17}},"loc":{"start":{"line":66,"column":61},"end":{"line":78,"column":5}}}},"branchMap":{"0":{"loc":{"start":{"line":48,"column":28},"end":{"line":48,"column":78}},"type":"binary-expr","locations":[{"start":{"line":48,"column":28},"end":{"line":48,"column":44}},{"start":{"line":48,"column":48},"end":{"line":48,"column":78}}]},"1":{"loc":{"start":{"line":54,"column":35},"end":{"line":54,"column":66}},"type":"binary-expr","locations":[{"start":{"line":54,"column":35},"end":{"line":54,"column":58}},{"start":{"line":54,"column":62},"end":{"line":54,"column":66}}]},"2":{"loc":{"start":{"line":67,"column":8},"end":{"line":67,"column":27}},"type":"if","locations":[{"start":{"line":67,"column":8},"end":{"line":67,"column":27}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":70,"column":12},"end":{"line":71,"column":51}},"type":"binary-expr","locations":[{"start":{"line":70,"column":12},"end":{"line":70,"column":54}},{"start":{"line":71,"column":12},"end":{"line":71,"column":51}}]},"4":{"loc":{"start":{"line":73,"column":8},"end":{"line":73,"column":32}},"type":"if","locations":[{"start":{"line":73,"column":8},"end":{"line":73,"column":32}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":22,"column":34},"end":{"line":22,"column":null}},"type":"cond-expr","locations":[{"start":{"line":22,"column":54},"end":{"line":22,"column":null}},{"start":{"line":22,"column":54},"end":{"line":22,"column":null}}]},"6":{"loc":{"start":{"line":22,"column":34},"end":{"line":22,"column":54}},"type":"binary-expr","locations":[{"start":{"line":22,"column":34},"end":{"line":22,"column":null}},{"start":{"line":22,"column":34},"end":{"line":22,"column":54}}]},"7":{"loc":{"start":{"line":22,"column":54},"end":{"line":24,"column":null}},"type":"cond-expr","locations":[{"start":{"line":24,"column":72},"end":{"line":24,"column":null}},{"start":{"line":24,"column":72},"end":{"line":24,"column":null}}]},"8":{"loc":{"start":{"line":22,"column":54},"end":{"line":24,"column":72}},"type":"binary-expr","locations":[{"start":{"line":22,"column":54},"end":{"line":24,"column":null}},{"start":{"line":24,"column":36},"end":{"line":24,"column":72}}]}},"s":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":4,"6":4,"7":4,"8":3,"9":3,"10":3,"11":1,"12":3,"13":4,"14":2,"15":2,"16":4,"17":1,"18":3,"19":3,"20":2,"21":1,"22":2,"23":2},"f":{"0":4,"1":4,"2":1,"3":4,"4":2,"5":4},"b":{"0":[4,3],"1":[2,1],"2":[1,3],"3":[3,2],"4":[2,1],"5":[0,2],"6":[2,0],"7":[0,2],"8":[2,0]}} +} diff --git a/apps/api/test/unit/coverage/lcov-report/base.css b/apps/api/test/unit/coverage/lcov-report/base.css new file mode 100644 index 00000000..f418035b --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/apps/api/test/unit/coverage/lcov-report/block-navigation.js b/apps/api/test/unit/coverage/lcov-report/block-navigation.js new file mode 100644 index 00000000..530d1ed2 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/apps/api/test/unit/coverage/lcov-report/favicon.png b/apps/api/test/unit/coverage/lcov-report/favicon.png new file mode 100644 index 00000000..c1525b81 Binary files /dev/null and b/apps/api/test/unit/coverage/lcov-report/favicon.png differ diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/cycle-stage-transitions.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/cycle-stage-transitions.ts.html new file mode 100644 index 00000000..02f01779 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/cycle-stage-transitions.ts.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for feedback360/application/constants/cycle-stage-transitions.ts + + + + + + + + + +
+
+

All files / feedback360/application/constants cycle-stage-transitions.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +3319x +  +  +  +  +  +19x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +19x +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+ 
+/**
+ * State machine transition map for Cycle
+ * Defines which stage transitions are allowed
+ */
+export const CYCLE_STAGE_TRANSITIONS: Record<CycleStage, CycleStage[]> = {
+    [CycleStage.NEW]: [CycleStage.ACTIVE, CycleStage.CANCELED],
+ 
+    [CycleStage.ACTIVE]: [CycleStage.FINISHED, CycleStage.CANCELED],
+ 
+    [CycleStage.FINISHED]: [CycleStage.PREPARING_REPORT, CycleStage.ARCHIVED],
+ 
+    [CycleStage.PREPARING_REPORT]: [CycleStage.PUBLISHED, CycleStage.ARCHIVED],
+ 
+    [CycleStage.PUBLISHED]: [CycleStage.ARCHIVED],
+ 
+    [CycleStage.CANCELED]: [CycleStage.ARCHIVED],
+ 
+    [CycleStage.ARCHIVED]: [],
+};
+ 
+/**
+ * Transition reasons for automated stage changes
+ */
+export const TRANSITION_REASONS = {
+    CYCLE_UPDATED: 'Cycle updated',
+    ALL_REVIEWS_COLLECTED: 'All reviews collected',
+    DEADLINE_REACHED: 'Cycle deadline has been reached',
+    FORCE_FINISH: 'Manually finished by HR or Admin',
+    SYSTEM_AUTOMATED: 'Automated system transition',
+} as const;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/index.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/index.html new file mode 100644 index 00000000..39066ac2 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for feedback360/application/constants + + + + + + + + + +
+
+

All files feedback360/application/constants

+
+ +
+ 100% + Statements + 9/9 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 9/9 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage-transitions.ts +
+
100%3/3100%0/0100%0/0100%3/3
response-status-transitions.ts +
+
100%3/3100%0/0100%0/0100%3/3
review-stage-transitions.ts +
+
100%3/3100%0/0100%0/0100%3/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/response-status-transitions.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/response-status-transitions.ts.html new file mode 100644 index 00000000..6db8f5a5 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/response-status-transitions.ts.html @@ -0,0 +1,187 @@ + + + + + + Code coverage report for feedback360/application/constants/response-status-transitions.ts + + + + + + + + + +
+
+

All files / feedback360/application/constants response-status-transitions.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +358x +  +  +  +  +  +8x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +  +  +  +  +  + 
import { ResponseStatus } from '@intra/shared-kernel';
+ 
+/**
+ * State machine transition map for respondent response status
+ * Defines which stage transitions are allowed
+ */
+export const RESPONSE_STATUS_TRANSITIONS: Record<
+    ResponseStatus,
+    ResponseStatus[]
+> = {
+    [ResponseStatus.PENDING]: [
+        ResponseStatus.IN_PROGRESS,
+        ResponseStatus.CANCELED,
+    ],
+ 
+    [ResponseStatus.IN_PROGRESS]: [
+        ResponseStatus.COMPLETED,
+        ResponseStatus.CANCELED,
+    ],
+ 
+    [ResponseStatus.COMPLETED]: [],
+ 
+    [ResponseStatus.CANCELED]: [],
+};
+ 
+/**
+ * Transition reasons for automated stage changes
+ */
+export const TRANSITION_REASONS = {
+    RESPONSE_ASSIGNED: 'HR or Admin assigned review to respondent',
+    RESPONSE_ADDED: 'Respondent added response',
+    RESPONSE_CANCELED: 'HR or Admin or Respondent canceled review assignment',
+    SYSTEM_AUTOMATED: 'Automated system transition',
+} as const;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/review-stage-transitions.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/review-stage-transitions.ts.html new file mode 100644 index 00000000..310bd390 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/constants/review-stage-transitions.ts.html @@ -0,0 +1,256 @@ + + + + + + Code coverage report for feedback360/application/constants/review-stage-transitions.ts + + + + + + + + + +
+
+

All files / feedback360/application/constants review-stage-transitions.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +588x +  +  +  +  +  +8x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+ 
+/**
+ * State machine transition map for Review lifecycle
+ * Defines which stage transitions are allowed
+ */
+export const REVIEW_STAGE_TRANSITIONS: Record<ReviewStage, ReviewStage[]> = {
+    [ReviewStage.NEW]: [ReviewStage.SELF_ASSESSMENT, ReviewStage.CANCELED],
+ 
+    [ReviewStage.SELF_ASSESSMENT]: [
+        ReviewStage.WAITING_TO_START,
+        ReviewStage.IN_PROGRESS,
+        ReviewStage.CANCELED,
+    ],
+ 
+    [ReviewStage.WAITING_TO_START]: [
+        ReviewStage.IN_PROGRESS,
+        ReviewStage.FINISHED,
+        ReviewStage.CANCELED,
+    ],
+ 
+    [ReviewStage.IN_PROGRESS]: [ReviewStage.FINISHED, ReviewStage.CANCELED],
+ 
+    [ReviewStage.FINISHED]: [ReviewStage.PREPARING_REPORT],
+ 
+    [ReviewStage.PREPARING_REPORT]: [
+        ReviewStage.PROCESSING_BY_HR,
+        ReviewStage.PUBLISHED,
+        ReviewStage.ANALYSIS,
+        ReviewStage.ARCHIVED,
+    ],
+ 
+    [ReviewStage.PROCESSING_BY_HR]: [
+        ReviewStage.PUBLISHED,
+        ReviewStage.ANALYSIS,
+        ReviewStage.ARCHIVED,
+    ],
+ 
+    [ReviewStage.PUBLISHED]: [ReviewStage.ANALYSIS, ReviewStage.ARCHIVED],
+ 
+    [ReviewStage.ANALYSIS]: [ReviewStage.ARCHIVED],
+ 
+    [ReviewStage.CANCELED]: [ReviewStage.ARCHIVED],
+ 
+    [ReviewStage.ARCHIVED]: [],
+};
+ 
+/**
+ * Transition reasons for automated stage changes
+ */
+export const TRANSITION_REASONS = {
+    REVIEW_UPDATED: 'Review updated',
+    ALL_RESPONSES_COLLECTED: 'All responses collected from respondents',
+    DEADLINE_REACHED: 'Review deadline has been reached',
+    FORCE_FINISH: 'Manually finished by HR or Admin',
+    SYSTEM_AUTOMATED: 'Automated system transition',
+} as const;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/cycle-stage-changed.event.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/cycle-stage-changed.event.ts.html new file mode 100644 index 00000000..7ff33a43 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/cycle-stage-changed.event.ts.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for feedback360/application/events/cycle-stage-changed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events cycle-stage-changed.event.ts

+
+ +
+ 100% + Statements + 7/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 7/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +19x +  +15x +15x +15x +15x +15x +15x +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+ 
+export class CycleStageChangedEvent {
+    constructor(
+        public readonly cycleId: number,
+        public readonly fromStage: CycleStage,
+        public readonly toStage: CycleStage,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/cycle-stage-processed.event.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/cycle-stage-processed.event.ts.html new file mode 100644 index 00000000..65b9137f --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/cycle-stage-processed.event.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for feedback360/application/events/cycle-stage-processed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events cycle-stage-processed.event.ts

+
+ +
+ 100% + Statements + 7/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 7/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +  +3x +  +9x +9x +9x +9x +9x +  +  +  +  +  +6x +  +  +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+import { CycleStageChangedEvent } from './cycle-stage-changed.event';
+ 
+export class CycleStageProcessedEvent {
+    constructor(
+        public readonly cycleId: number,
+        public readonly currentStage: CycleStage,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+ 
+    static fromCycleStageChangedEvent(
+        event: CycleStageChangedEvent,
+    ): CycleStageProcessedEvent {
+        return new CycleStageProcessedEvent(
+            event.cycleId,
+            event.toStage,
+            event.changedById,
+            event.changedByName,
+            event.reason,
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/index.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/index.html new file mode 100644 index 00000000..a4144d86 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/index.html @@ -0,0 +1,176 @@ + + + + + + Code coverage report for feedback360/application/events + + + + + + + + + +
+
+

All files feedback360/application/events

+
+ +
+ 100% + Statements + 36/36 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 36/36 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage-changed.event.ts +
+
100%7/7100%0/0100%1/1100%7/7
cycle-stage-processed.event.ts +
+
100%7/7100%0/0100%2/2100%7/7
respondent-status-changed.event.ts +
+
100%8/8100%0/0100%1/1100%8/8
review-stage-changed.event.ts +
+
100%7/7100%0/0100%1/1100%7/7
review-stage-processed.event.ts +
+
100%7/7100%0/0100%2/2100%7/7
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/respondent-status-changed.event.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/respondent-status-changed.event.ts.html new file mode 100644 index 00000000..657c18fc --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/respondent-status-changed.event.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for feedback360/application/events/respondent-status-changed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events respondent-status-changed.event.ts

+
+ +
+ 100% + Statements + 8/8 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 8/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +7x +  +12x +12x +12x +12x +12x +12x +12x +  +  + 
import { ResponseStatus } from '@intra/shared-kernel';
+ 
+export class RespondentStatusChangedEvent {
+    constructor(
+        public readonly reviewId: number,
+        public readonly respondentRelationId: number,
+        public readonly fromStatus: ResponseStatus,
+        public readonly toStatus: ResponseStatus,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/review-stage-changed.event.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/review-stage-changed.event.ts.html new file mode 100644 index 00000000..b85a9014 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/review-stage-changed.event.ts.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for feedback360/application/events/review-stage-changed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events review-stage-changed.event.ts

+
+ +
+ 100% + Statements + 7/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 7/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +10x +  +18x +18x +18x +18x +18x +18x +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+ 
+export class ReviewStageChangedEvent {
+    constructor(
+        public readonly reviewId: number,
+        public readonly fromStage: ReviewStage,
+        public readonly toStage: ReviewStage,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/review-stage-processed.event.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/review-stage-processed.event.ts.html new file mode 100644 index 00000000..3615eded --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/events/review-stage-processed.event.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for feedback360/application/events/review-stage-processed.event.ts + + + + + + + + + +
+
+

All files / feedback360/application/events review-stage-processed.event.ts

+
+ +
+ 100% + Statements + 7/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 7/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +  +3x +  +11x +11x +11x +11x +11x +  +  +  +  +  +8x +  +  +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+import { ReviewStageChangedEvent } from './review-stage-changed.event';
+ 
+export class ReviewStageProcessedEvent {
+    constructor(
+        public readonly reviewId: number,
+        public readonly currentStage: ReviewStage,
+        public readonly changedById?: number | null,
+        public readonly changedByName?: string | null,
+        public readonly reason?: string | null,
+    ) {}
+ 
+    static fromReviewStageChangedEvent(
+        event: ReviewStageChangedEvent,
+    ): ReviewStageProcessedEvent {
+        return new ReviewStageProcessedEvent(
+            event.reviewId,
+            event.toStage,
+            event.changedById,
+            event.changedByName,
+            event.reason,
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/cycle-stage.listener.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/cycle-stage.listener.ts.html new file mode 100644 index 00000000..66497a13 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/cycle-stage.listener.ts.html @@ -0,0 +1,340 @@ + + + + + + Code coverage report for feedback360/application/listeners/cycle-stage.listener.ts + + + + + + + + + +
+
+

All files / feedback360/application/listeners cycle-stage.listener.ts

+
+ +
+ 100% + Statements + 30/30 +
+ + +
+ 75% + Branches + 18/24 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 28/28 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +861x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +1x +4x +  +  +4x +4x +  +4x +  +  +  +  +  +  +  +  +1x +  +  +4x +  +  +  +4x +  +1x +  +  +  +1x +  +  +3x +  +3x +1x +  +  +1x +  +  +  +1x +  +  +2x +  +  +  +2x +2x +  +1x +  +  +  +1x +  +  +  +  +2x +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+import { Inject, Injectable, Logger } from '@nestjs/common';
+import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
+import { CycleStageChangedEvent } from '../../../feedback360/application/events/cycle-stage-changed.event';
+import { ClusterScoreAnalyticsService } from '../../../feedback360/application/services/cluster-score-analytics.service';
+import { CycleStageProcessedEvent } from '../events/cycle-stage-processed.event';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+ 
+/**
+ * Event listener for cycle stage changes
+ * Automatically triggers cluster score analytics generation for the cycle
+ * when it transitions to the FINISHED stage
+ */
+@Injectable()
+export class CycleStageListener {
+    private readonly logger = new Logger(CycleStageListener.name);
+ 
+    constructor(
+        private readonly analytics: ClusterScoreAnalyticsService,
+        private readonly eventEmitter: EventEmitter2,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+    ) {}
+ 
+    /*
+     * Handles the cycle.stage.changed event
+     * Generates cluster score analytics for the cycle
+     * when it transitions to the FINISHED stage
+     */
+    @OnEvent('cycle.stage.changed')
+    async handleCycleStageChanged(
+        event: CycleStageChangedEvent,
+    ): Promise<void> {
+        this.logger.debug(
+            `Cycle ${event.cycleId} transitioned from ${event.fromStage} to ${event.toStage}`,
+        );
+ 
+        if (event.toStage !== CycleStage.FINISHED) {
+            // Emit event for listeners to react
+            this.eventEmitter.emit(
+                'cycle.stage.processed',
+                CycleStageProcessedEvent.fromCycleStageChangedEvent(event),
+            );
+            return;
+        }
+ 
+        const reviews = await this.reviews.listByCycleId(event.cycleId);
+ 
+        if (reviews.length === 0) {
+            this.logger.debug(
+                `No reviews found for cycle ${event.cycleId}. Skipping cluster score analytics generation.`,
+            );
+            this.eventEmitter.emit(
+                'cycle.stage.processed',
+                CycleStageProcessedEvent.fromCycleStageChangedEvent(event),
+            );
+            return;
+        }
+ 
+        this.logger.debug(
+            `Initiating cluster score analytics generation for cycle ${event.cycleId}`,
+        );
+ 
+        try {
+            await this.analytics.generateAnalyticsForCycle(event.cycleId);
+ 
+            this.logger.debug(
+                `Successfully generated cluster score analytics for cycle ${event.cycleId}`,
+            );
+        } catch (error) {
+            this.logger.error(
+                `Failed to generate cluster score analytics for cycle ${event.cycleId}: ${error.message}`,
+            );
+        } finally {
+            // Emit event for listeners to react
+            this.eventEmitter.emit(
+                'cycle.stage.processed',
+                CycleStageProcessedEvent.fromCycleStageChangedEvent(event),
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/index.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/index.html new file mode 100644 index 00000000..7d19a844 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for feedback360/application/listeners + + + + + + + + + +
+
+

All files feedback360/application/listeners

+
+ +
+ 100% + Statements + 122/122 +
+ + +
+ 78.07% + Branches + 89/114 +
+ + +
+ 100% + Functions + 11/11 +
+ + +
+ 100% + Lines + 114/114 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage.listener.ts +
+
100%30/3075%18/24100%2/2100%28/28
respondent-status.listener.ts +
+
100%53/5379.03%49/62100%6/6100%49/49
review-stage.listener.ts +
+
100%39/3978.57%22/28100%3/3100%37/37
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/respondent-status.listener.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/respondent-status.listener.ts.html new file mode 100644 index 00000000..a5051fc3 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/respondent-status.listener.ts.html @@ -0,0 +1,583 @@ + + + + + + Code coverage report for feedback360/application/listeners/respondent-status.listener.ts + + + + + + + + + +
+
+

All files / feedback360/application/listeners respondent-status.listener.ts

+
+ +
+ 100% + Statements + 53/53 +
+ + +
+ 79.03% + Branches + 49/62 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 49/49 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +1671x +  +  +  +  +1x +1x +1x +1x +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +1x +4x +  +  +  +4x +4x +4x +  +  +  +  +  +  +  +  +1x +  +  +4x +  +  +  +4x +  +  +  +1x +  +  +3x +  +  +  +3x +3x +  +  +  +2x +  +4x +  +  +  +2x +1x +1x +  +  +  +1x +  +  +  +2x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +6x +  +  +  +6x +  +6x +6x +  +  +  +1x +  +  +6x +1x +  +  +5x +5x +  +  +  +5x +1x +  +  +4x +4x +1x +  +  +1x +  +  +3x +1x +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  + 
import {
+    RespondentCategory,
+    ResponseStatus,
+    ReviewStage,
+} from '@intra/shared-kernel';
+import { Inject, Injectable, Logger } from '@nestjs/common';
+import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
+import { TRANSITION_REASONS } from '../constants/review-stage-transitions';
+import { RespondentStatusChangedEvent } from '../events/respondent-status-changed.event';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../ports/respondent.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+import { ReviewService } from '../services/review.service';
+ 
+/**
+ * Event listener for respondent status changes
+ * Automatically triggers review completion when all responses are collected
+ */
+@Injectable()
+export class RespondentStatusListener {
+    private readonly logger = new Logger(RespondentStatusListener.name);
+ 
+    constructor(
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        private readonly reviews: ReviewService,
+        private readonly eventEmitter: EventEmitter2,
+    ) {}
+ 
+    /*
+     * Handles the respondent.status.changed event
+     * Changes review status to finished
+     * if all responses for the review are collected
+     */
+    @OnEvent('respondent.status.changed')
+    async handleRespondentStatusChanged(
+        event: RespondentStatusChangedEvent,
+    ): Promise<void> {
+        this.logger.debug(
+            `Review response ${event.respondentRelationId} transitioned from ${event.fromStatus} to ${event.toStatus}`,
+        );
+ 
+        if (
+            event.toStatus !== ResponseStatus.COMPLETED &&
+            event.toStatus !== ResponseStatus.CANCELED
+        ) {
+            return;
+        }
+ 
+        this.logger.debug(
+            `Checking if all responses for review ${event.reviewId} are collected`,
+        );
+ 
+        try {
+            const respondents = await this.respondents.listByReview(
+                event.reviewId,
+                {},
+            );
+            const allCompleted = respondents.every(
+                (respondent) =>
+                    respondent.responseStatus === ResponseStatus.COMPLETED ||
+                    respondent.responseStatus === ResponseStatus.CANCELED,
+            );
+ 
+            if (allCompleted) {
+                await this.reviews.completeReview(event.reviewId);
+                this.logger.debug(
+                    `Successfully completed review ${event.reviewId} due to all responses being collected`,
+                );
+            } else {
+                this.logger.debug(
+                    `Review ${event.reviewId} is not completed yet. Remaining responses: ${
+                        respondents.filter(
+                            (respondent) =>
+                                respondent.responseStatus ===
+                                    ResponseStatus.PENDING ||
+                                respondent.responseStatus ===
+                                    ResponseStatus.IN_PROGRESS,
+                        ).length
+                    }`,
+                );
+            }
+        } catch (error) {
+            this.logger.error(
+                `Failed to check response collecting completion for review ${event.reviewId}: ${error.message}`,
+                error.stack,
+            );
+        }
+    }
+}
+ 
+/**
+ * Event listener that auto-transitions a review from SELF_ASSESSMENT to
+ * IN_PROGRESS once the ratee has submitted their self-assessment response.
+ *
+ * Trigger: respondent.status.changed → toStatus = COMPLETED
+ *          AND respondent.category = SELF_ASSESSMENT
+ *          AND review.stage = SELF_ASSESSMENT.
+ */
+@Injectable()
+export class SelfAssessmentCompletedListener {
+    private readonly logger = new Logger(SelfAssessmentCompletedListener.name);
+ 
+    constructor(
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        private readonly reviewService: ReviewService,
+    ) {}
+ 
+    @OnEvent('respondent.status.changed')
+    async handleRespondentCompleted(
+        event: RespondentStatusChangedEvent,
+    ): Promise<void> {
+        if (event.toStatus !== ResponseStatus.COMPLETED) {
+            return;
+        }
+ 
+        try {
+            const respondent = await this.respondents.getById(
+                event.respondentRelationId,
+            );
+ 
+            if (respondent.category !== RespondentCategory.SELF_ASSESSMENT) {
+                return;
+            }
+ 
+            const review = await this.reviews.findById(event.reviewId);
+            if (!review) {
+                this.logger.warn(
+                    `Review ${event.reviewId} not found while handling self-assessment completion`,
+                );
+                return;
+            }
+ 
+            if (review.stage !== ReviewStage.SELF_ASSESSMENT) {
+                return;
+            }
+ 
+            this.logger.debug(
+                `Ratee submitted self-assessment for review ${event.reviewId}; advancing stage SELF_ASSESSMENT → IN_PROGRESS`,
+            );
+ 
+            await this.reviewService.changeReviewStage(
+                event.reviewId,
+                ReviewStage.IN_PROGRESS,
+                event.changedById ?? undefined,
+                event.changedByName ?? undefined,
+                TRANSITION_REASONS.SYSTEM_AUTOMATED,
+            );
+        } catch (error) {
+            this.logger.error(
+                `Failed to auto-advance review ${event.reviewId} to IN_PROGRESS after self-assessment completion: ${
+                    (error as Error).message
+                }`,
+                (error as Error).stack,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/review-stage.listener.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/review-stage.listener.ts.html new file mode 100644 index 00000000..7af12498 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/listeners/review-stage.listener.ts.html @@ -0,0 +1,460 @@ + + + + + + Code coverage report for feedback360/application/listeners/review-stage.listener.ts + + + + + + + + + +
+
+

All files / feedback360/application/listeners review-stage.listener.ts

+
+ +
+ 100% + Statements + 39/39 +
+ + +
+ 78.57% + Branches + 22/28 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 37/37 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +1261x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +1x +6x +  +  +6x +  +6x +6x +  +  +  +  +  +  +  +  +1x +  +  +6x +  +  +  +6x +6x +1x +  +  +5x +  +5x +1x +1x +  +  +4x +1x +  +  +1x +  +  +3x +  +  +  +3x +3x +  +  +  +  +  +  +  +  +  +  +  +  +3x +6x +  +3x +  +3x +2x +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +6x +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+import { Inject, Injectable, Logger } from '@nestjs/common';
+import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
+import { ReviewStageChangedEvent } from '../../../feedback360/application/events/review-stage-changed.event';
+import { CycleService } from '../../../feedback360/application/services/cycle.service';
+import { ReviewStageProcessedEvent } from '../events/review-stage-processed.event';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+ 
+/**
+ * Stages where a review is still actively collecting responses.
+ * A cycle is considered ready to complete when no review is in any of
+ * these stages — i.e. every review has moved past response collection,
+ * regardless of whether it is currently FINISHED, in the report
+ * generation pipeline (PREPARING_REPORT / PROCESSING_BY_HR), already
+ * PUBLISHED / ANALYSIS / ARCHIVED, or CANCELED.
+ */
+const ACTIVE_REVIEW_STAGES: readonly ReviewStage[] = [
+    ReviewStage.NEW,
+    ReviewStage.SELF_ASSESSMENT,
+    ReviewStage.WAITING_TO_START,
+    ReviewStage.IN_PROGRESS,
+];
+ 
+/**
+ * Event listener for review stage changes
+ * Automatically triggers cycle completion when all reviews are finished
+ */
+@Injectable()
+export class ReviewStageListener {
+    private readonly logger = new Logger(ReviewStageListener.name);
+ 
+    constructor(
+        private readonly cycles: CycleService,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        private readonly eventEmitter: EventEmitter2,
+    ) {}
+ 
+    /**
+     * Handles review.stage.changed events
+     * Changes cycle status to finished
+     * if all reviews for the cycle are finished
+     */
+    @OnEvent('review.stage.changed')
+    async handleReviewStageChanged(
+        event: ReviewStageChangedEvent,
+    ): Promise<void> {
+        this.logger.debug(
+            `Review ${event.reviewId} transitioned from ${event.fromStage} to ${event.toStage}`,
+        );
+ 
+        try {
+            if (event.toStage !== ReviewStage.FINISHED) {
+                return;
+            }
+ 
+            const review = await this.reviews.findById(event.reviewId);
+ 
+            if (!review) {
+                this.logger.warn(`Review ${event.reviewId} not found`);
+                return;
+            }
+ 
+            if (!review.cycleId) {
+                this.logger.debug(
+                    `Review ${event.reviewId} has no cycle attached; skipping cycle completion check`,
+                );
+                return;
+            }
+ 
+            this.logger.debug(
+                `Check if all reviews for cycle ${review.cycleId} are finished`,
+            );
+ 
+            try {
+                const cycleReviews = await this.reviews.listByCycleId(
+                    review.cycleId,
+                );
+                // A cycle is "completable" once no review is still in an
+                // active (response-collection) stage. We treat anything
+                // past IN_PROGRESS — including PREPARING_REPORT,
+                // PROCESSING_BY_HR, PUBLISHED, ANALYSIS, ARCHIVED, and
+                // CANCELED — as already done from the cycle's POV. This
+                // matters because individual reviews are auto-progressed
+                // through the report pipeline as soon as they reach
+                // FINISHED, so by the time the LAST review in the cycle
+                // hits FINISHED, all earlier ones are already in
+                // PROCESSING_BY_HR or further.
+                const remainingActive = cycleReviews.filter((cycleReview) =>
+                    ACTIVE_REVIEW_STAGES.includes(cycleReview.stage),
+                );
+                const allCompleted = remainingActive.length === 0;
+ 
+                if (allCompleted) {
+                    await this.cycles.completeCycle(review.cycleId);
+                    this.logger.debug(
+                        `Successfully completed cycle ${review.cycleId} due to all reviews being completed`,
+                    );
+                } else {
+                    this.logger.debug(
+                        `Cycle ${review.cycleId} is not completed yet. Remaining reviews: ${remainingActive.length}`,
+                    );
+                }
+            } catch (error) {
+                this.logger.error(
+                    `Failed to check review collecting completion for cycle ${review.cycleId}: ${error.message}`,
+                    error.stack,
+                );
+            }
+        } finally {
+            // Always notify downstream listeners (e.g. report generation)
+            // regardless of whether the review had a cycle attached or
+            // whether the cycle completion check succeeded. Skipping this
+            // emit would prevent individual report generation for reviews
+            // that aren't part of a cycle.
+            this.eventEmitter.emit(
+                'review.stage.processed',
+                ReviewStageProcessedEvent.fromReviewStageChangedEvent(event),
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/answer.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/answer.repository.port.ts.html new file mode 100644 index 00000000..b2bd69cc --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/answer.repository.port.ts.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for feedback360/application/ports/answer.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports answer.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +  +  +18x +  +  +  +  +  +  +  +  +  +  + 
import { AnswerSearchQuery, RespondentCategory } from '@intra/shared-kernel';
+import { AnswerDomain } from '../../domain/answer.domain';
+ 
+export const ANSWER_REPOSITORY = Symbol('FEEDBACK360.ANSWER_REPOSITORY');
+ 
+export interface AnswerRepositoryPort {
+    create(answer: AnswerDomain): Promise<AnswerDomain>;
+    list(query: AnswerSearchQuery): Promise<AnswerDomain[]>;
+    findById(id: number): Promise<AnswerDomain | null>;
+    getAnswersCountByRespondentCategories(
+        reviewId: number,
+    ): Promise<{ respondentCategory: RespondentCategory; answers: number }[]>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cluster-score-analytics.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cluster-score-analytics.repository.port.ts.html new file mode 100644 index 00000000..d660a4b8 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cluster-score-analytics.repository.port.ts.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for feedback360/application/ports/cluster-score-analytics.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports cluster-score-analytics.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    ClusterScoreAnalyticsSearchQuery,
+    UpdateClusterScoreAnalyticsPayload,
+} from '@intra/shared-kernel';
+import { ClusterScoreAnalyticsDomain } from '../../domain/cluster-score-analytics.domain';
+ 
+export const CLUSTER_SCORE_ANALYTICS_REPOSITORY = Symbol(
+    'FEEDBACK360.CLUSTER_SCORE_ANALYTICS_REPOSITORY',
+);
+ 
+export interface ClusterScoreAnalyticsRepositoryPort {
+    upsert(
+        analytics: ClusterScoreAnalyticsDomain,
+    ): Promise<ClusterScoreAnalyticsDomain>;
+    findById(id: number): Promise<ClusterScoreAnalyticsDomain | null>;
+    search(
+        query: ClusterScoreAnalyticsSearchQuery,
+    ): Promise<ClusterScoreAnalyticsDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdateClusterScoreAnalyticsPayload,
+    ): Promise<ClusterScoreAnalyticsDomain>;
+    deleteById(id: number): Promise<void>;
+    getByCycleId(cycleId: number): Promise<ClusterScoreAnalyticsDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cluster-score.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cluster-score.repository.port.ts.html new file mode 100644 index 00000000..3bfedef9 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cluster-score.repository.port.ts.html @@ -0,0 +1,130 @@ + + + + + + Code coverage report for feedback360/application/ports/cluster-score.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports cluster-score.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16  +  +  +  +15x +  +  +  +  +  +  +  +  +  +  + 
import { ClusterScoreSearchQuery } from '@intra/shared-kernel';
+import { ClusterScoreWithRelationsDomain } from '../../domain/cluster-score-with-relations.domain';
+import { ClusterScoreDomain } from '../../domain/cluster-score.domain';
+ 
+export const CLUSTER_SCORE_REPOSITORY = Symbol(
+    'FEEDBACK360.CLUSTER_SCORE_REPOSITORY',
+);
+ 
+export interface ClusterScoreRepositoryPort {
+    upsert(score: ClusterScoreDomain): Promise<ClusterScoreDomain>;
+    list(query: ClusterScoreSearchQuery): Promise<ClusterScoreDomain[]>;
+    getById(id: number): Promise<ClusterScoreWithRelationsDomain>;
+    deleteById(id: number): Promise<void>;
+    getByCycleId(cycleId: number): Promise<ClusterScoreWithRelationsDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cycle-stage-history.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cycle-stage-history.repository.port.ts.html new file mode 100644 index 00000000..6568fdd4 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cycle-stage-history.repository.port.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for feedback360/application/ports/cycle-stage-history.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports cycle-stage-history.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +18x +  +  +  +  +  +  +  +  + 
import { CycleStageHistoryDomain } from '../../domain/cycle-stage-history.domain';
+ 
+export const CYCLE_STAGE_HISTORY_REPOSITORY = Symbol(
+    'FEEDBACK360.CYCLE_STAGE_HISTORY_REPOSITORY',
+);
+ 
+export interface CycleStageHistoryRepositoryPort {
+    create(history: CycleStageHistoryDomain): Promise<CycleStageHistoryDomain>;
+    findByCycleId(cycleId: number): Promise<CycleStageHistoryDomain[]>;
+    findById(id: number): Promise<CycleStageHistoryDomain | null>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cycle.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cycle.repository.port.ts.html new file mode 100644 index 00000000..19317ed3 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/cycle.repository.port.ts.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for feedback360/application/ports/cycle.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports cycle.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +  +27x +  +  +  +  +  +  +  +  + 
import { CycleSearchQuery, UpdateCyclePayload } from '@intra/shared-kernel';
+import { CycleDomain } from '../../domain/cycle.domain';
+ 
+export const CYCLE_REPOSITORY = Symbol('FEEDBACK360.CYCLE_REPOSITORY');
+ 
+export interface CycleRepositoryPort {
+    create(cycle: CycleDomain): Promise<CycleDomain>;
+    findById(id: number): Promise<CycleDomain | null>;
+    search(query: CycleSearchQuery): Promise<CycleDomain[]>;
+    updateById(id: number, patch: UpdateCyclePayload): Promise<CycleDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/index.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/index.html new file mode 100644 index 00000000..05a42317 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/index.html @@ -0,0 +1,266 @@ + + + + + + Code coverage report for feedback360/application/ports + + + + + + + + + +
+
+

All files feedback360/application/ports

+
+ +
+ 100% + Statements + 11/11 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 11/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
answer.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
cluster-score-analytics.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
cluster-score.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
cycle-stage-history.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
cycle.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
question.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
respondent.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
review-question-relation.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
review-stage-history.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
review.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
reviewer.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/question.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/question.repository.port.ts.html new file mode 100644 index 00000000..48b5a533 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/question.repository.port.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for feedback360/application/ports/question.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports question.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +14x +  +  +  +  +  +  +  + 
import { QuestionSearchQuery } from '@intra/shared-kernel';
+import { QuestionDomain } from '../../domain/question.domain';
+ 
+export const QUESTION_REPOSITORY = Symbol('FEEDBACK360.QUESTION_REPOSITORY');
+ 
+export interface QuestionRepositoryPort {
+    create(question: QuestionDomain): Promise<QuestionDomain>;
+    findById(id: number): Promise<QuestionDomain | null>;
+    search(query: QuestionSearchQuery): Promise<QuestionDomain[]>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/respondent.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/respondent.repository.port.ts.html new file mode 100644 index 00000000..fad6fead --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/respondent.repository.port.ts.html @@ -0,0 +1,154 @@ + + + + + + Code coverage report for feedback360/application/ports/respondent.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports respondent.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24  +  +  +  +  +  +21x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    RespondentSearchQuery,
+    UpdateRespondentPayload,
+} from '@intra/shared-kernel';
+import { RespondentDomain } from '../../domain/respondent.domain';
+ 
+export const RESPONDENT_REPOSITORY = Symbol(
+    'FEEDBACK360.RESPONDENT_REPOSITORY',
+);
+ 
+export interface RespondentRepositoryPort {
+    create(relation: RespondentDomain): Promise<RespondentDomain>;
+    getById(id: number): Promise<RespondentDomain>;
+    listByReview(
+        reviewId: number,
+        query: RespondentSearchQuery,
+    ): Promise<RespondentDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdateRespondentPayload,
+    ): Promise<RespondentDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review-question-relation.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review-question-relation.repository.port.ts.html new file mode 100644 index 00000000..03a94288 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review-question-relation.repository.port.ts.html @@ -0,0 +1,136 @@ + + + + + + Code coverage report for feedback360/application/ports/review-question-relation.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports review-question-relation.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18  +  +  +13x +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { ReviewQuestionRelationSearchQuery } from '@intra/shared-kernel';
+import { ReviewQuestionRelationDomain } from '../../domain/review-question-relation.domain';
+ 
+export const REVIEW_QUESTION_RELATION_REPOSITORY = Symbol(
+    'FEEDBACK360.REVIEW_QUESTION_RELATION_REPOSITORY',
+);
+ 
+export interface ReviewQuestionRelationRepositoryPort {
+    link(
+        relation: ReviewQuestionRelationDomain,
+    ): Promise<ReviewQuestionRelationDomain>;
+    listByReview(
+        reviewId: number,
+        query: ReviewQuestionRelationSearchQuery,
+    ): Promise<ReviewQuestionRelationDomain[]>;
+    unlink(reviewId: number, questionId: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review-stage-history.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review-stage-history.repository.port.ts.html new file mode 100644 index 00000000..9dcbec39 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review-stage-history.repository.port.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for feedback360/application/ports/review-stage-history.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports review-stage-history.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +7x +  +  +  +  +  +  +  +  +  +  + 
import { ReviewStageHistoryDomain } from '../../domain/review-stage-history.domain';
+ 
+export const REVIEW_STAGE_HISTORY_REPOSITORY = Symbol(
+    'FEEDBACK360.REVIEW_STAGE_HISTORY_REPOSITORY',
+);
+ 
+export interface ReviewStageHistoryRepositoryPort {
+    create(
+        history: ReviewStageHistoryDomain,
+    ): Promise<ReviewStageHistoryDomain>;
+    findByReviewId(reviewId: number): Promise<ReviewStageHistoryDomain[]>;
+    findById(id: number): Promise<ReviewStageHistoryDomain | null>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review.repository.port.ts.html new file mode 100644 index 00000000..c6dc6ad2 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/review.repository.port.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for feedback360/application/ports/review.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports review.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +  +27x +  +  +  +  +  +  +  +  +  + 
import { ReviewSearchQuery, UpdateReviewPayload } from '@intra/shared-kernel';
+import { ReviewDomain } from '../../domain/review.domain';
+ 
+export const REVIEW_REPOSITORY = Symbol('FEEDBACK360.REVIEW_REPOSITORY');
+ 
+export interface ReviewRepositoryPort {
+    create(review: ReviewDomain): Promise<ReviewDomain>;
+    findById(id: number): Promise<ReviewDomain | null>;
+    search(query: ReviewSearchQuery): Promise<ReviewDomain[]>;
+    updateById(id: number, patch: UpdateReviewPayload): Promise<ReviewDomain>;
+    deleteById(id: number): Promise<void>;
+    listByCycleId(cycleId: number): Promise<ReviewDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/reviewer.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/reviewer.repository.port.ts.html new file mode 100644 index 00000000..1c318cdc --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/ports/reviewer.repository.port.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for feedback360/application/ports/reviewer.repository.port.ts + + + + + + + + + +
+
+

All files / feedback360/application/ports reviewer.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +  +21x +  +  +  +  +  +  +  +  +  + 
import { ReviewerSearchQuery } from '@intra/shared-kernel';
+import { ReviewerDomain } from '../../domain/reviewer.domain';
+ 
+export const REVIEWER_REPOSITORY = Symbol('FEEDBACK360.REVIEWER_REPOSITORY');
+ 
+export interface ReviewerRepositoryPort {
+    create(relation: ReviewerDomain): Promise<ReviewerDomain>;
+    listByReview(
+        reviewId: number,
+        query: ReviewerSearchQuery,
+    ): Promise<ReviewerDomain[]>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/cluster-score-analytics.service.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/cluster-score-analytics.service.ts.html new file mode 100644 index 00000000..dbfea535 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/cluster-score-analytics.service.ts.html @@ -0,0 +1,838 @@ + + + + + + Code coverage report for feedback360/application/services/cluster-score-analytics.service.ts + + + + + + + + + +
+
+

All files / feedback360/application/services cluster-score-analytics.service.ts

+
+ +
+ 98.75% + Statements + 79/80 +
+ + +
+ 72% + Branches + 36/50 +
+ + +
+ 100% + Functions + 17/17 +
+ + +
+ 98.71% + Lines + 77/78 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +2523x +  +  +  +  +  +3x +  +  +  +  +  +3x +3x +3x +3x +  +  +  +3x +  +  +  +3x +  +  +3x +  +  +18x +  +18x +18x +18x +  +  +  +  +  +6x +6x +6x +6x +6x +6x +  +6x +3x +  +1x +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +6x +6x +2x +  +  +4x +  +  +  +  +  +  +2x +  +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +2x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +2x +1x +  +  +  +  +  +1x +  +  +  +  +  +  +  +4x +  +4x +4x +  +  +  +4x +5x +  +  +  +5x +10x +  +  +5x +1x +  +  +4x +7x +  +4x +4x +4x +7x +  +  +4x +  +4x +6x +6x +4x +  +10x +10x +  +4x +  +  +  +4x +4x +  +4x +3x +  +3x +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +2x +  +  +  +6x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +10x +2x +  +  +  +8x +1x +  +  +  +7x +1x +  +  +  +  +  + 
import {
+    CLUSTER_SCORE_ANALYTICS_CONSTRAINTS,
+    ClusterScoreAnalyticsSearchQuery,
+    UpdateClusterScoreAnalyticsPayload,
+    UpsertClusterScoreAnalyticsPayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import Decimal from 'decimal.js';
+import { ClusterService } from '../../../library/application/services/cluster.service';
+import { ClusterScoreAnalyticsDomain } from '../../domain/cluster-score-analytics.domain';
+import {
+    CLUSTER_SCORE_ANALYTICS_REPOSITORY,
+    ClusterScoreAnalyticsRepositoryPort,
+} from '../ports/cluster-score-analytics.repository.port';
+import {
+    CLUSTER_SCORE_REPOSITORY,
+    ClusterScoreRepositoryPort,
+} from '../ports/cluster-score.repository.port';
+import { CycleService } from './cycle.service';
+ 
+@Injectable()
+export class ClusterScoreAnalyticsService {
+    constructor(
+        @Inject(CLUSTER_SCORE_ANALYTICS_REPOSITORY)
+        private readonly analytics: ClusterScoreAnalyticsRepositoryPort,
+        @Inject(CLUSTER_SCORE_REPOSITORY)
+        private readonly clusterScores: ClusterScoreRepositoryPort,
+        private readonly cycles: CycleService,
+        private readonly clusters: ClusterService,
+    ) {}
+ 
+    async upsert(
+        payload: UpsertClusterScoreAnalyticsPayload,
+    ): Promise<ClusterScoreAnalyticsDomain> {
+        await this.cycles.getById(payload.cycleId);
+        const minScore = new Decimal(payload.minScore);
+        const maxScore = new Decimal(payload.maxScore);
+        const averageScore = new Decimal(payload.averageScore);
+        const lowerBound = new Decimal(payload.lowerBound);
+        const upperBound = new Decimal(payload.upperBound);
+ 
+        await this.validateBounds(lowerBound, upperBound);
+        await this.validateScores(minScore, maxScore, averageScore);
+ 
+        const domain = ClusterScoreAnalyticsDomain.create({
+            cycleId: payload.cycleId,
+            clusterId: payload.clusterId,
+            employeesCount: payload.employeesCount,
+            employeeDensity: payload.employeeDensity,
+            lowerBound,
+            upperBound,
+            minScore,
+            maxScore,
+            averageScore,
+        });
+ 
+        return this.analytics.upsert(domain);
+    }
+ 
+    async search(
+        query: ClusterScoreAnalyticsSearchQuery,
+    ): Promise<ClusterScoreAnalyticsDomain[]> {
+        return this.analytics.search(query);
+    }
+ 
+    async getById(id: number): Promise<ClusterScoreAnalyticsDomain> {
+        const item = await this.analytics.findById(id);
+        if (!item)
+            throw new NotFoundException(
+                'Cycle cluster analytics with id ' + id + ' not found',
+            );
+        return item;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateClusterScoreAnalyticsPayload,
+    ): Promise<ClusterScoreAnalyticsDomain> {
+        const current = await this.getById(id);
+ 
+        const minScore =
+            patch.minScore !== undefined
+                ? new Decimal(patch.minScore)
+                : current.minScore;
+        const maxScore =
+            patch.maxScore !== undefined
+                ? new Decimal(patch.maxScore)
+                : current.maxScore;
+        const averageScore =
+            patch.averageScore !== undefined
+                ? new Decimal(patch.averageScore)
+                : current.averageScore;
+ 
+        await this.validateScores(minScore, maxScore, averageScore);
+ 
+        const payload: UpdateClusterScoreAnalyticsPayload = {
+            ...(patch.employeesCount !== undefined
+                ? { employeesCount: patch.employeesCount }
+                : {}),
+            ...(patch.employeeDensity !== undefined
+                ? { employeeDensity: patch.employeeDensity }
+                : {}),
+            ...(patch.minScore !== undefined
+                ? { minScore: patch.minScore }
+                : {}),
+            ...(patch.maxScore !== undefined
+                ? { maxScore: patch.maxScore }
+                : {}),
+            ...(patch.averageScore !== undefined
+                ? { averageScore: patch.averageScore }
+                : {}),
+        };
+ 
+        return this.analytics.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        await this.analytics.deleteById(id);
+    }
+ 
+    async getByCycleId(
+        cycleId: number,
+    ): Promise<ClusterScoreAnalyticsDomain[]> {
+        return this.analytics.getByCycleId(cycleId);
+    }
+ 
+    /**
+     * Generates analytics metrics for a cycle.
+     * @param cycleId The cycle identifier.
+     */
+    async generateAnalyticsForCycle(cycleId: number): Promise<void> {
+        await this.cycles.getById(cycleId);
+ 
+        const clusters = await this.clusters.search({});
+        const clusterScores = await this.clusterScores.list({
+            cycleId,
+        });
+ 
+        for (const cluster of clusters) {
+            Iif (!cluster.id) {
+                continue;
+            }
+ 
+            const currentClusterScores = clusterScores.filter(
+                (cs) => cs.clusterId === cluster.id,
+            );
+ 
+            if (!currentClusterScores.length) {
+                continue;
+            }
+ 
+            const scores = currentClusterScores.map(
+                (cs) => new Decimal(cs.score),
+            );
+            const minScore = Decimal.min(...scores).toDecimalPlaces(4);
+            const maxScore = Decimal.max(...scores).toDecimalPlaces(4);
+            const averageScore = scores
+                .reduce((a, b) => a.plus(b), new Decimal(0))
+                .dividedBy(scores.length)
+                .toDecimalPlaces(4);
+            const employeesCount = currentClusterScores.length;
+ 
+            const competenceClusterIds = clusters
+                .filter((c) => c.competenceId === cluster.competenceId)
+                .map((c) => c.id);
+            const rateesByCompetence = new Set(
+                clusterScores
+                    .filter((cs) => competenceClusterIds.includes(cs.clusterId))
+                    .map((cs) => cs.rateeId),
+            );
+            const employeeDensity = new Decimal(currentClusterScores.length)
+                .dividedBy(rateesByCompetence.size)
+                .toDecimalPlaces(4);
+ 
+            const lowerBound = new Decimal(cluster.lowerBound);
+            const upperBound = new Decimal(cluster.upperBound);
+ 
+            await this.validateBounds(lowerBound, upperBound);
+            await this.validateScores(minScore, maxScore, averageScore);
+ 
+            const domain = ClusterScoreAnalyticsDomain.create({
+                cycleId,
+                clusterId: cluster.id,
+                employeesCount,
+                employeeDensity,
+                lowerBound,
+                upperBound,
+                minScore,
+                maxScore,
+                averageScore,
+            });
+ 
+            await this.analytics.upsert(domain);
+        }
+    }
+ 
+    /**
+     * Validates the scores for a cluster.
+     * @param min The minimum score.
+     * @param max The maximum score.
+     * @param avg The average score.
+     */
+    private async validateScores(
+        min: Decimal,
+        max: Decimal,
+        avg: Decimal,
+    ): Promise<void> {
+        if (min.gt(max)) {
+            throw new BadRequestException(
+                'Min score must be less than or equal to max score',
+            );
+        }
+        if (avg.lt(min) || avg.gt(max)) {
+            throw new BadRequestException(
+                'Average score must be between min and max scores',
+            );
+        }
+    }
+ 
+    /**
+     * Validates the bounds for a cluster.
+     * @param lower The lower bound.
+     * @param upper The upper bound.
+     */
+    private async validateBounds(
+        lower: Decimal,
+        upper: Decimal,
+    ): Promise<void> {
+        if (lower.lt(CLUSTER_SCORE_ANALYTICS_CONSTRAINTS.SCORE.MIN)) {
+            throw new BadRequestException(
+                'Lower bound must be greater than or equal to min score',
+            );
+        }
+        if (upper.gt(CLUSTER_SCORE_ANALYTICS_CONSTRAINTS.SCORE.MAX)) {
+            throw new BadRequestException(
+                'Upper bound must be less than or equal to max score',
+            );
+        }
+        if (lower.gt(upper)) {
+            throw new BadRequestException(
+                'Lower bound must be less than or equal to upper bound',
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/cycle.service.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/cycle.service.ts.html new file mode 100644 index 00000000..d74dd0ba --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/cycle.service.ts.html @@ -0,0 +1,1024 @@ + + + + + + Code coverage report for feedback360/application/services/cycle.service.ts + + + + + + + + + +
+
+

All files / feedback360/application/services cycle.service.ts

+
+ +
+ 100% + Statements + 71/71 +
+ + +
+ 78.65% + Branches + 70/89 +
+ + +
+ 100% + Functions + 13/13 +
+ + +
+ 100% + Lines + 69/69 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +31418x +  +  +  +  +  +  +  +  +18x +  +  +  +  +  +18x +18x +  +  +  +18x +18x +18x +18x +  +  +  +18x +18x +  +  +  +18x +  +  +  +18x +  +  +  +  +  +18x +  +  +21x +  +21x +  +21x +21x +21x +  +21x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +  +  +  +1x +  +  +  +24x +24x +2x +22x +  +  +  +6x +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x +  +6x +1x +  +  +  +  +  +  +  +5x +  +5x +  +3x +3x +2x +  +  +  +  +  +3x +  +3x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +2x +1x +  +  +2x +1x +  +  +  +  +  +  +  +  +  +4x +  +  +  +2x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +7x +7x +  +  +7x +  +7x +1x +  +  +  +  +  +6x +  +6x +  +  +  +  +  +6x +  +  +  +  +  +  +  +  +6x +  +  +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +4x +  +  +4x +  +  +4x +  +4x +  +  +4x +  +  +4x +2x +  +  +  +  +  +  +2x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  + 
import {
+    CreateCyclePayload,
+    CYCLE_CONSTRAINTS,
+    CycleSearchQuery,
+    CycleStage,
+    ReviewStage,
+    SYSTEM_ACTOR,
+    UpdateCyclePayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { EventEmitter2 } from '@nestjs/event-emitter';
+import {
+    STRATEGIC_REPORT_REPOSITORY,
+    StrategicReportRepositoryPort,
+} from 'src/contexts/reporting/application/ports/strategic-report.repository.port';
+import { PrismaService } from 'src/database/prisma.service';
+import { CycleStageHistoryDomain } from '../../domain/cycle-stage-history.domain';
+import { CycleDomain } from '../../domain/cycle.domain';
+import {
+    CYCLE_STAGE_TRANSITIONS,
+    TRANSITION_REASONS,
+} from '../constants/cycle-stage-transitions';
+import { CycleStageChangedEvent } from '../events/cycle-stage-changed.event';
+import {
+    CYCLE_STAGE_HISTORY_REPOSITORY,
+    CycleStageHistoryRepositoryPort,
+} from '../ports/cycle-stage-history.repository.port';
+import {
+    CYCLE_REPOSITORY,
+    CycleRepositoryPort,
+} from '../ports/cycle.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+ 
+@Injectable()
+export class CycleService {
+    constructor(
+        @Inject(CYCLE_REPOSITORY)
+        private readonly cycles: CycleRepositoryPort,
+        @Inject(CYCLE_STAGE_HISTORY_REPOSITORY)
+        private readonly stageHistory: CycleStageHistoryRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        private readonly eventEmitter: EventEmitter2,
+        private readonly prisma: PrismaService,
+        @Inject(STRATEGIC_REPORT_REPOSITORY)
+        private readonly strategicReports: StrategicReportRepositoryPort,
+    ) {}
+ 
+    async create(payload: CreateCyclePayload): Promise<CycleDomain> {
+        const cycle = CycleDomain.create({
+            title: payload.title,
+            description: payload.description,
+            hrId: payload.hrId,
+            stage: payload.stage ?? CycleStage.NEW,
+            minRespondentsThreshold:
+                payload.minRespondentsThreshold ??
+                CYCLE_CONSTRAINTS.ANONYMITY_THRESHOLD.MIN,
+            isActive: payload.isActive ?? true,
+            startDate: payload.startDate,
+            reviewDeadline: payload.reviewDeadline,
+            approvalDeadline: payload.approvalDeadline,
+            responseDeadline: payload.responseDeadline,
+            endDate: payload.endDate,
+        });
+ 
+        const created = await this.cycles.create(cycle);
+        return this.getById(created.id!);
+    }
+ 
+    async search(query: CycleSearchQuery): Promise<CycleDomain[]> {
+        return this.cycles.search(query);
+    }
+ 
+    async getById(id: number): Promise<CycleDomain> {
+        const cycle = await this.cycles.findById(id);
+        if (!cycle)
+            throw new NotFoundException('Cycle with id ' + id + ' not found');
+        return cycle;
+    }
+ 
+    async update(id: number, patch: UpdateCyclePayload): Promise<CycleDomain> {
+        const cycle = await this.getById(id);
+ 
+        const payload: UpdateCyclePayload = {
+            ...(patch.title !== undefined ? { title: patch.title } : {}),
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+            ...(patch.hrId !== undefined ? { hrId: patch.hrId } : {}),
+            ...(patch.isActive !== undefined
+                ? { isActive: patch.isActive }
+                : {}),
+            ...(patch.startDate !== undefined
+                ? { startDate: patch.startDate }
+                : {}),
+            ...(patch.reviewDeadline !== undefined
+                ? { reviewDeadline: patch.reviewDeadline }
+                : {}),
+            ...(patch.approvalDeadline !== undefined
+                ? { approvalDeadline: patch.approvalDeadline }
+                : {}),
+            ...(patch.responseDeadline !== undefined
+                ? { responseDeadline: patch.responseDeadline }
+                : {}),
+            ...(patch.endDate !== undefined ? { endDate: patch.endDate } : {}),
+        };
+ 
+        // Check if cycle has strategic report and title is being updated
+        const strategicReport = await this.strategicReports.findByCycleId(id);
+ 
+        if (strategicReport !== null && payload.title) {
+            throw new BadRequestException(
+                'Cycle ' +
+                    cycle.id +
+                    ' has strategic report. Cannot update. Current stage: ' +
+                    cycle.stage,
+            );
+        }
+ 
+        const updated = await this.cycles.updateById(id, payload);
+ 
+        if (patch.stage && (patch.stage as CycleStage) !== cycle.stage) {
+            // Check if cycle has incomplete reviews and stage is being updated to a final stage
+            const reviews = await this.reviews.listByCycleId(id);
+            const incompleteReviews = reviews.filter((r) =>
+                [
+                    ReviewStage.SELF_ASSESSMENT,
+                    ReviewStage.IN_PROGRESS,
+                    ReviewStage.PROCESSING_BY_HR,
+                ].includes(r.stage),
+            );
+            const hasInProgressReviews = incompleteReviews.length > 0;
+ 
+            if (
+                hasInProgressReviews &&
+                patch.stage &&
+                [
+                    CycleStage.CANCELED,
+                    CycleStage.FINISHED,
+                    CycleStage.PREPARING_REPORT,
+                    CycleStage.PUBLISHED,
+                    CycleStage.ARCHIVED,
+                ].includes(patch.stage)
+            ) {
+                throw new BadRequestException(
+                    'Cycle ' +
+                        cycle.id +
+                        ' has ' +
+                        incompleteReviews.length +
+                        ' active or processing by HR reviews. Cannot update. Current cycle stage: ' +
+                        cycle.stage,
+                );
+            }
+ 
+            // Reactive Trigger: Check if all responses completed
+            if (patch.stage && patch.stage === CycleStage.FINISHED) {
+                await this.completeCycle(id);
+            }
+ 
+            if (patch.stage && patch.stage !== CycleStage.PUBLISHED) {
+                await this.changeStage(
+                    id,
+                    patch.stage,
+                    SYSTEM_ACTOR.ID,
+                    SYSTEM_ACTOR.NAME,
+                    TRANSITION_REASONS.CYCLE_UPDATED,
+                );
+            }
+        }
+ 
+        return updated;
+    }
+ 
+    async delete(id: number): Promise<void> {
+        const cycle = await this.getById(id);
+ 
+        await this.cycles.deleteById(id);
+    }
+ 
+    /**
+     * Centralized method for stage transitions with validation and history tracking
+     * @param cycleId ID of the cycle
+     * @param nextStage Target stage
+     * @param actorId User ID who initiated the change (0 for system)
+     * @param actorName Full name of the actor
+     * @param reason Optional reason for the transition
+     */
+    async changeStage(
+        cycleId: number,
+        nextStage: CycleStage,
+        actorId: number = SYSTEM_ACTOR.ID,
+        actorName: string | null = SYSTEM_ACTOR.NAME,
+        reason?: string,
+    ): Promise<void> {
+        const cycle = await this.getById(cycleId);
+        const currentStage = cycle.stage;
+ 
+        // Validate transition is allowed
+        const allowedTransitions = CYCLE_STAGE_TRANSITIONS[currentStage];
+ 
+        if (!allowedTransitions.includes(nextStage)) {
+            throw new BadRequestException(
+                `Invalid cycle stage transition from ${currentStage} to ${nextStage}`,
+            );
+        }
+ 
+        // Perform update and history logging in a transaction
+        await this.prisma.$transaction(async (tx) => {
+            // Update cycle stage
+            await tx.cycle.update({
+                where: { id: cycleId },
+                data: { stage: nextStage },
+            });
+ 
+            // Create history record
+            const history = CycleStageHistoryDomain.create({
+                cycleId,
+                fromStage: currentStage,
+                toStage: nextStage,
+                changedById: actorId || null,
+                changedByName: actorName,
+                reason,
+            });
+ 
+            await this.stageHistory.create(history);
+        });
+ 
+        // Emit event for listeners to react
+        this.eventEmitter.emit(
+            'cycle.stage.changed',
+            new CycleStageChangedEvent(
+                cycleId,
+                currentStage,
+                nextStage,
+                actorId,
+                actorName,
+                reason,
+            ),
+        );
+    }
+ 
+    /**
+     * Get stage change history for a cycle
+     */
+    async getStageHistory(cycleId: number): Promise<CycleStageHistoryDomain[]> {
+        return this.stageHistory.findByCycleId(cycleId);
+    }
+ 
+    /**
+     * REACTIVE TRIGGER: Checks if all review for a cycle have collected.
+     * If yes, automatically transitions the cycle to the FINISHED stage.
+     * @param cycleId The cycle identifier.
+     */
+    async completeCycle(cycleId: number): Promise<void> {
+        const cycle = await this.getById(cycleId);
+ 
+        // Get all reviews for this cycle
+        const reviews = await this.reviews.listByCycleId(cycleId);
+ 
+        // Check if there are any in-progress reviews
+        const incompleteReviews = reviews.filter(
+            (r) =>
+                r.stage === ReviewStage.SELF_ASSESSMENT ||
+                r.stage === ReviewStage.IN_PROGRESS,
+        );
+        const hasInProgressReviews = incompleteReviews.length > 0;
+ 
+        // If no in-progress reviews, trigger report generation
+        if (!hasInProgressReviews && reviews.length > 0) {
+            await this.changeStage(
+                cycleId,
+                CycleStage.FINISHED,
+                SYSTEM_ACTOR.ID,
+                SYSTEM_ACTOR.NAME,
+                TRANSITION_REASONS.ALL_REVIEWS_COLLECTED,
+            );
+        } else if (hasInProgressReviews && reviews.length > 0) {
+            throw new BadRequestException(
+                'All reviews must be completed to finish cycle #' +
+                    cycleId +
+                    '. Incomplete reviews count: ' +
+                    incompleteReviews.length,
+            );
+        }
+    }
+ 
+    /**
+     * MANUAL TRIGGER: HR or ADMIN force-finishes a cycle
+     * Transitions cycle to FINISHED regardless of in-progress reviews
+     */
+    async forceFinish(
+        cycleId: number,
+        actorId: number = SYSTEM_ACTOR.ID,
+        actorName: string | null = SYSTEM_ACTOR.NAME,
+    ): Promise<void> {
+        await this.getById(cycleId);
+        await this.changeStage(
+            cycleId,
+            CycleStage.FINISHED,
+            actorId,
+            actorName,
+            TRANSITION_REASONS.FORCE_FINISH,
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/index.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/index.html new file mode 100644 index 00000000..a7b6433d --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for feedback360/application/services + + + + + + + + + +
+
+

All files feedback360/application/services

+
+ +
+ 97.17% + Statements + 447/460 +
+ + +
+ 78.92% + Branches + 352/446 +
+ + +
+ 100% + Functions + 75/75 +
+ + +
+ 99.3% + Lines + 426/429 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cluster-score-analytics.service.ts +
+
98.75%79/8072%36/50100%17/1798.71%77/78
cycle.service.ts +
+
100%71/7178.65%70/89100%13/13100%69/69
review-scheduler.service.ts +
+
100%47/4785.71%24/28100%3/3100%45/45
review.service.ts +
+
95.41%250/26279.56%222/279100%42/4299.15%235/237
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/review-scheduler.service.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/review-scheduler.service.ts.html new file mode 100644 index 00000000..638068e1 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/review-scheduler.service.ts.html @@ -0,0 +1,541 @@ + + + + + + Code coverage report for feedback360/application/services/review-scheduler.service.ts + + + + + + + + + +
+
+

All files / feedback360/application/services review-scheduler.service.ts

+
+ +
+ 100% + Statements + 47/47 +
+ + +
+ 85.71% + Branches + 24/28 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 45/45 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +1531x +  +  +  +  +  +1x +1x +1x +1x +1x +1x +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +10x +  +  +10x +10x +  +  +  +  +  +  +  +1x +5x +  +5x +  +5x +  +  +  +4x +4x +1x +  +  +3x +3x +  +  +3x +  +2x +  +  +  +  +  +  +  +2x +  +  +  +  +2x +4x +4x +  +  +  +  +  +  +3x +  +  +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +1x +5x +  +5x +5x +  +  +  +  +4x +  +4x +  +5x +  +5x +  +  +5x +1x +  +  +4x +4x +3x +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  + 
import {
+    CycleStage,
+    IdentityRole,
+    ReviewStage,
+    SYSTEM_ACTOR,
+} from '@intra/shared-kernel';
+import { Injectable, Logger } from '@nestjs/common';
+import { Cron, CronExpression } from '@nestjs/schedule';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { TRANSITION_REASONS } from '../constants/review-stage-transitions';
+import { CycleService } from './cycle.service';
+import { ReviewService } from './review.service';
+ 
+/**
+ * Synthetic actor used by the scheduler when calling access-checked
+ * methods on `ReviewService`
+ */
+const SYSTEM_USER = UserDomain.create({
+    id: SYSTEM_ACTOR.ID,
+    firstName: SYSTEM_ACTOR.NAME,
+    lastName: '',
+    email: 'system@intra.local',
+    roles: [IdentityRole.ADMIN],
+});
+ 
+/**
+ * Scheduled service for checking review deadlines.
+ * Runs periodically to transition reviews to FINISHED once their cycle's
+ * `responseDeadline` has passed. The review state machine only allows
+ * IN_PROGRESS → FINISHED | CANCELED — moving directly to PREPARING_REPORT
+ * is not a valid transition, so the deadline trigger lands the review in
+ * FINISHED and the report-generation flow takes it from there.
+ */
+@Injectable()
+export class ReviewSchedulerService {
+    private readonly logger = new Logger(ReviewSchedulerService.name);
+ 
+    constructor(
+        private readonly reviewService: ReviewService,
+        private readonly cycleService: CycleService,
+    ) {}
+ 
+    /**
+     * SCHEDULED TRIGGER: Check for reviews past their deadline
+     * Runs every day at 7am to find reviews that should be completed
+     */
+    @Cron(CronExpression.EVERY_DAY_AT_7AM)
+    async checkReviewDeadlines(): Promise<void> {
+        this.logger.log('Checking for reviews past deadline...');
+ 
+        try {
+            // Get all active cycles
+            const cycles = await this.cycleService.search({
+                isActive: true,
+            });
+ 
+            for (const cycle of cycles) {
+                if (!cycle.responseDeadline) {
+                    continue;
+                }
+ 
+                const now = new Date();
+                const deadline = new Date(cycle.responseDeadline);
+ 
+                // If deadline has passed
+                if (now > deadline) {
+                    // Find all reviews in this cycle that are still IN_PROGRESS.
+                    const reviews = await this.reviewService.search(
+                        {
+                            cycleId: cycle.id!,
+                            stage: ReviewStage.IN_PROGRESS,
+                        },
+                        SYSTEM_USER,
+                    );
+ 
+                    this.logger.log(
+                        `Found ${reviews.length} reviews past deadline in cycle #${cycle.id}`,
+                    );
+ 
+                    // Transition each review to FINISHED
+                    for (const review of reviews) {
+                        try {
+                            await this.reviewService.changeReviewStage(
+                                review.id!,
+                                ReviewStage.FINISHED,
+                                SYSTEM_ACTOR.ID,
+                                SYSTEM_ACTOR.NAME,
+                                TRANSITION_REASONS.DEADLINE_REACHED,
+                            );
+                            this.logger.log(
+                                `Transitioned review #${review.id} to FINISHED (deadline)`,
+                            );
+                        } catch (error) {
+                            this.logger.error(
+                                `Failed to transition review #${review.id}: ${error.message}`,
+                            );
+                        }
+                    }
+                }
+            }
+        } catch (error) {
+            this.logger.error(
+                `Error checking review deadlines: ${error.message}`,
+            );
+        }
+    }
+ 
+    /**
+     * SCHEDULED TRIGGER: Finish cycles past their deadlines
+     * Runs every day at 7am to find cycles that should be finished
+     */
+    @Cron(CronExpression.EVERY_DAY_AT_7AM)
+    async checkCycleDeadlines(): Promise<void> {
+        this.logger.log('Checking for cycles past deadline...');
+ 
+        try {
+            const cycles = await this.cycleService.search({
+                stage: CycleStage.ACTIVE,
+                isActive: true,
+            });
+ 
+            const now = new Date();
+ 
+            for (const cycle of cycles) {
+                const endDatePassed =
+                    cycle.endDate && new Date(cycle.endDate) < now;
+                const responseDeadlinePassed =
+                    cycle.responseDeadline &&
+                    new Date(cycle.responseDeadline) < now;
+ 
+                if (!endDatePassed && !responseDeadlinePassed) {
+                    continue;
+                }
+ 
+                try {
+                    await this.cycleService.forceFinish(cycle.id!);
+                    this.logger.log(
+                        `Finished cycle #${cycle.id} (deadline reached)`,
+                    );
+                } catch (error) {
+                    this.logger.error(
+                        `Failed to finish cycle #${cycle.id}: ${error.message}`,
+                    );
+                }
+            }
+        } catch (error) {
+            this.logger.error(
+                `Error checking cycle deadlines: ${error.message}`,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/review.service.ts.html b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/review.service.ts.html new file mode 100644 index 00000000..bc7b99e2 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/feedback360/application/services/review.service.ts.html @@ -0,0 +1,3160 @@ + + + + + + Code coverage report for feedback360/application/services/review.service.ts + + + + + + + + + +
+
+

All files / feedback360/application/services review.service.ts

+
+ +
+ 95.41% + Statements + 250/262 +
+ + +
+ 79.56% + Branches + 222/279 +
+ + +
+ 100% + Functions + 42/42 +
+ + +
+ 99.15% + Lines + 235/237 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +10267x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +7x +  +  +  +  +  +  +7x +  +7x +7x +7x +7x +  +7x +7x +7x +7x +7x +7x +7x +7x +  +  +  +7x +  +  +  +7x +7x +7x +  +  +  +7x +  +  +  +7x +  +  +  +7x +  +  +  +7x +  +  +  +7x +  +  +  +7x +  +  +  +7x +  +  +  +7x +  +  +7x +  +  +65x +  +65x +  +65x +  +65x +  +65x +  +65x +  +65x +  +65x +65x +65x +65x +65x +65x +  +  +  +2x +1x +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +  +  +  +  +  +  +2x +  +2x +  +  +4x +  +  +  +4x +  +  +4x +  +2x +  +  +  +48x +48x +1x +  +47x +  +46x +  +  +  +  +  +  +  +  +  +  +  +  +6x +  +  +6x +  +  +6x +  +4x +4x +  +4x +  +2x +  +  +  +2x +  +1x +  +  +  +  +  +  +  +  +5x +  +  +5x +  +  +5x +  +2x +2x +  +2x +  +1x +  +  +  +1x +  +1x +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +47x +  +  +9x +  +  +9x +  +5x +5x +  +5x +  +3x +  +  +  +3x +  +2x +  +  +  +2x +  +1x +  +  +  +  +  +  +  +  +  +4x +  +4x +  +  +  +1x +  +  +  +  +  +  +  +3x +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +2x +  +  +  +  +  +  +  +2x +  +  +  +  +1x +  +  +  +  +  +  +  +  +2x +  +  +  +2x +  +2x +2x +1x +  +  +  +  +  +  +1x +  +  +  +  +  +3x +2x +2x +  +  +  +1x +  +  +  +  +  +  +  +  +2x +  +  +  +2x +2x +  +2x +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +1x +  +  +  +3x +  +3x +1x +  +  +  +  +2x +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +3x +  +3x +1x +  +  +  +  +  +  +  +2x +  +  +2x +  +  +  +  +  +  +2x +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +1x +1x +  +  +  +2x +  +2x +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +2x +  +2x +  +  +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +4x +4x +  +3x +  +  +  +  +  +  +2x +2x +  +  +2x +  +2x +  +  +  +  +  +2x +  +2x +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +3x +  +2x +2x +  +2x +  +  +  +1x +  +  +  +  +  +  +  +1x +1x +  +1x +  +1x +1x +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +4x +4x +1x +  +  +  +3x +  +  +3x +3x +1x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +3x +  +3x +  +3x +  +3x +  +1x +  +  +  +  +  +  +  +  +  +  +  +1x +  +1x +  +  +  +  +  +1x +  +  +  +1x +  +  +  +1x +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +2x +1x +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +2x +  +  +2x +  +1x +  +1x +1x +  +1x +  +1x +  +  +  +1x +  +1x +  +  +  +1x +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +2x +1x +  +  +2x +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +5x +  +  +5x +  +5x +1x +  +  +  +  +  +4x +  +4x +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +4x +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +2x +  +  +2x +  +  +2x +  +3x +  +  +2x +  +  +2x +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  + 
import {
+    AddQuestionToReviewPayload,
+    AddRespondentPayload,
+    AddReviewerPayload,
+    ClusterScoreSearchQuery,
+    CreateAnswerPayload,
+    CreateQuestionPayload,
+    CreateReviewPayload,
+    CycleStage,
+    IdentityRole,
+    QuestionSearchQuery,
+    RespondentCategory,
+    RespondentSearchQuery,
+    ResponseStatus,
+    ReviewerSearchQuery,
+    ReviewQuestionRelationSearchQuery,
+    ReviewSearchQuery,
+    ReviewStage,
+    SYSTEM_ACTOR,
+    UpdateRespondentPayload,
+    UpdateReviewPayload,
+    UpsertClusterScorePayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    ForbiddenException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { EventEmitter2 } from '@nestjs/event-emitter';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { CompetenceService } from 'src/contexts/library/application/services/competence.service';
+import { QuestionTemplateService } from 'src/contexts/library/application/services/question-template.service';
+import { PrismaService } from 'src/database/prisma.service';
+import { AnswerDomain } from '../../domain/answer.domain';
+import { ClusterScoreWithRelationsDomain } from '../../domain/cluster-score-with-relations.domain';
+import { ClusterScoreDomain } from '../../domain/cluster-score.domain';
+import { QuestionDomain } from '../../domain/question.domain';
+import { RespondentDomain } from '../../domain/respondent.domain';
+import { ReviewQuestionRelationDomain } from '../../domain/review-question-relation.domain';
+import { ReviewStageHistoryDomain } from '../../domain/review-stage-history.domain';
+import { ReviewDomain } from '../../domain/review.domain';
+import { ReviewerDomain } from '../../domain/reviewer.domain';
+import {
+    RESPONSE_STATUS_TRANSITIONS,
+    TRANSITION_REASONS as RESPONSE_TRANSITION_REASONS,
+} from '../constants/response-status-transitions';
+import {
+    REVIEW_STAGE_TRANSITIONS,
+    TRANSITION_REASONS as REVIEW_TRANSITION_REASONS,
+} from '../constants/review-stage-transitions';
+import { RespondentStatusChangedEvent } from '../events/respondent-status-changed.event';
+import { ReviewStageChangedEvent } from '../events/review-stage-changed.event';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../ports/answer.repository.port';
+import {
+    CLUSTER_SCORE_REPOSITORY,
+    ClusterScoreRepositoryPort,
+} from '../ports/cluster-score.repository.port';
+import {
+    QuestionRepositoryPort,
+    QUESTION_REPOSITORY as REVIEW_QUESTION_REPOSITORY,
+} from '../ports/question.repository.port';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../ports/respondent.repository.port';
+import {
+    REVIEW_QUESTION_RELATION_REPOSITORY,
+    ReviewQuestionRelationRepositoryPort,
+} from '../ports/review-question-relation.repository.port';
+import {
+    REVIEW_STAGE_HISTORY_REPOSITORY,
+    ReviewStageHistoryRepositoryPort,
+} from '../ports/review-stage-history.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../ports/review.repository.port';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from '../ports/reviewer.repository.port';
+import { CycleService } from './cycle.service';
+ 
+@Injectable()
+export class ReviewService {
+    constructor(
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        @Inject(REVIEW_QUESTION_REPOSITORY)
+        private readonly questions: QuestionRepositoryPort,
+        @Inject(REVIEW_QUESTION_RELATION_REPOSITORY)
+        private readonly questionRelations: ReviewQuestionRelationRepositoryPort,
+        @Inject(ANSWER_REPOSITORY)
+        private readonly answers: AnswerRepositoryPort,
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        @Inject(REVIEWER_REPOSITORY)
+        private readonly reviewers: ReviewerRepositoryPort,
+        @Inject(CLUSTER_SCORE_REPOSITORY)
+        private readonly clusterScores: ClusterScoreRepositoryPort,
+        @Inject(REVIEW_STAGE_HISTORY_REPOSITORY)
+        private readonly stageHistory: ReviewStageHistoryRepositoryPort,
+        private readonly prisma: PrismaService,
+        private readonly questionTemplates: QuestionTemplateService,
+        private readonly competences: CompetenceService,
+        private readonly cycles: CycleService,
+        private readonly eventEmitter: EventEmitter2,
+    ) {}
+ 
+    async create(payload: CreateReviewPayload): Promise<ReviewDomain> {
+        if (payload.cycleId) {
+            await this.cycles.getById(payload.cycleId);
+        }
+ 
+        const review = ReviewDomain.create({
+            rateeId: payload.rateeId,
+            rateeFullName: payload.rateeFullName,
+            rateePositionId: payload.rateePositionId,
+            rateePositionTitle: payload.rateePositionTitle,
+            hrId: payload.hrId,
+            hrFullName: payload.hrFullName,
+            hrNote: payload.hrNote,
+            teamId: payload.teamId ?? null,
+            teamTitle: payload.teamTitle ?? null,
+            managerId: payload.managerId ?? null,
+            managerFullName: payload.managerFullName ?? null,
+            managerPositionId: payload.managerPositionId ?? null,
+            managerPositionTitle: payload.managerPositionTitle ?? null,
+            cycleId: payload.cycleId ?? null,
+            stage: payload.stage ?? ReviewStage.NEW,
+            reportId: payload.reportId ?? null,
+        });
+ 
+        const created = await this.reviews.create(review);
+        return this.getById(created.id!);
+    }
+ 
+    async search(
+        query: ReviewSearchQuery,
+        actor?: UserDomain,
+    ): Promise<ReviewDomain[]> {
+        const reviews = await this.reviews.search(query);
+ 
+        const filteredReviews = (
+            await Promise.all(
+                reviews.map(async (review) => {
+                    const hasAccessToReview = await this.hasAccessToReadReview(
+                        review,
+                        actor,
+                    );
+                    return hasAccessToReview ? review : undefined;
+                }),
+            )
+        ).filter((review) => review !== undefined);
+ 
+        return filteredReviews;
+    }
+ 
+    async getById(id: number, actor?: UserDomain): Promise<ReviewDomain> {
+        const review = await this.reviews.findById(id);
+        if (!review)
+            throw new NotFoundException('Review with id ' + id + ' not found');
+ 
+        await this.checkAccessToReview(review, actor);
+ 
+        return review;
+    }
+ 
+    /**
+     * Checks if the actor has access to the review answers
+     * as an admin, HR, manager, ratee, or reviewer.
+     * @param review The review to check access for.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToReviewAnswers(
+        review: ReviewDomain,
+        actor?: UserDomain,
+    ): Promise<void> {
+        Iif (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        if (reviewers.length > 0) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view review #' + review.id + '.',
+        );
+    }
+ 
+    private async hasAccessToReadReview(
+        review: ReviewDomain,
+        actor?: UserDomain,
+    ): Promise<boolean> {
+        Iif (!actor) return false;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return true;
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return true;
+ 
+        const respondents = await this.respondents.listByReview(review.id!, {
+            respondentId: actor.id,
+        });
+ 
+        Iif (respondents.length > 0) return true;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        Iif (reviewers.length > 0) return true;
+ 
+        return false;
+    }
+ 
+    /**
+     * Checks if the actor has access to the review
+     * as an admin, HR, manager, ratee, respondent, or reviewer.
+     * @param review The review to check access for.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToReview(
+        review: ReviewDomain,
+        actor?: UserDomain,
+    ): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return;
+ 
+        const respondents = await this.respondents.listByReview(review.id!, {
+            respondentId: actor.id,
+        });
+ 
+        if (respondents.length > 0) return;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        if (reviewers.length > 0) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view review #' + review.id + '.',
+        );
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateReviewPayload,
+        actor: UserDomain,
+    ): Promise<ReviewDomain> {
+        const review = await this.getById(id);
+ 
+        if (
+            review.stage !== ReviewStage.NEW &&
+            review.stage !== ReviewStage.PROCESSING_BY_HR
+        ) {
+            throw new BadRequestException(
+                'Review #' +
+                    id +
+                    ' must be new or processing by HR to be updated. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        if (patch.cycleId) {
+            const cycle = await this.cycles.getById(patch.cycleId);
+            Eif (
+                cycle.stage !== CycleStage.NEW &&
+                cycle.stage !== CycleStage.ACTIVE
+            ) {
+                throw new BadRequestException(
+                    'Cycle #' +
+                        id +
+                        ' must be new or active to be assigned to review. Current stage: ' +
+                        cycle.stage,
+                );
+            }
+        }
+ 
+        const payload: UpdateReviewPayload = {
+            ...(patch.rateeId !== undefined ? { rateeId: patch.rateeId } : {}),
+            ...(patch.rateeFullName !== undefined
+                ? { rateeFullName: patch.rateeFullName }
+                : {}),
+            ...(patch.rateePositionId !== undefined
+                ? { rateePositionId: patch.rateePositionId }
+                : {}),
+            ...(patch.rateePositionTitle !== undefined
+                ? { rateePositionTitle: patch.rateePositionTitle }
+                : {}),
+            ...(patch.hrId !== undefined ? { hrId: patch.hrId } : {}),
+            ...(patch.hrFullName !== undefined
+                ? { hrFullName: patch.hrFullName }
+                : {}),
+            ...(patch.hrNote !== undefined ? { hrNote: patch.hrNote } : {}),
+            ...(patch.teamId !== undefined ? { teamId: patch.teamId } : {}),
+            ...(patch.teamTitle !== undefined
+                ? { teamTitle: patch.teamTitle }
+                : {}),
+            ...(patch.managerId !== undefined
+                ? { managerId: patch.managerId }
+                : {}),
+            ...(patch.managerFullName !== undefined
+                ? { managerFullName: patch.managerFullName }
+                : {}),
+            ...(patch.managerPositionId !== undefined
+                ? { managerPositionId: patch.managerPositionId }
+                : {}),
+            ...(patch.managerPositionTitle !== undefined
+                ? { managerPositionTitle: patch.managerPositionTitle }
+                : {}),
+            ...(patch.cycleId !== undefined ? { cycleId: patch.cycleId } : {}),
+            ...(patch.reportId !== undefined
+                ? { reportId: patch.reportId }
+                : {}),
+        };
+ 
+        const updatedReview = await this.reviews.updateById(id, payload);
+ 
+        // Reactive Trigger: Check if all responses completed
+        Iif (
+            patch.stage &&
+            patch.stage !== updatedReview.stage &&
+            patch.stage === ReviewStage.FINISHED
+        ) {
+            await this.completeReview(id);
+        }
+ 
+        if (
+            patch.stage &&
+            patch.stage !== updatedReview.stage &&
+            patch.stage !== ReviewStage.FINISHED
+        ) {
+            await this.changeReviewStage(
+                id,
+                patch.stage,
+                actor.id,
+                actor.fullName,
+                REVIEW_TRANSITION_REASONS.REVIEW_UPDATED,
+            );
+        }
+ 
+        return updatedReview;
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+ 
+        const existingAnswers = await this.answers.list({ reviewId: id });
+        if (existingAnswers.length > 0) {
+            throw new BadRequestException(
+                'Review #' +
+                    id +
+                    ' cannot be deleted. It already has answers submitted by respondents.',
+            );
+        }
+ 
+        await this.reviews.deleteById(id);
+    }
+ 
+    async createQuestion(
+        payload: CreateQuestionPayload,
+    ): Promise<QuestionDomain> {
+        if (payload.cycleId) {
+            const cycle = await this.cycles.getById(payload.cycleId);
+            if (
+                cycle.stage !== CycleStage.NEW &&
+                cycle.stage !== CycleStage.ACTIVE
+            ) {
+                throw new BadRequestException(
+                    'Cycle #' +
+                        cycle.id +
+                        ' must be in NEW or ACTIVE stage to be assigned to question. Current stage: ' +
+                        cycle.stage,
+                );
+            }
+        }
+ 
+        const baseQuestion = payload.questionTemplateId
+            ? await this.questionTemplates.getById(payload.questionTemplateId)
+            : null;
+ 
+        const title = payload.title ?? baseQuestion?.title;
+        const answerType = payload.answerType ?? baseQuestion?.answerType;
+ 
+        if (!title || !answerType) {
+            throw new NotFoundException(
+                'Base question data is required to create review question: title and answer type are required.',
+            );
+        }
+ 
+        const question = QuestionDomain.create({
+            cycleId: payload.cycleId ?? null,
+            questionTemplateId: payload.questionTemplateId ?? null,
+            title,
+            answerType,
+            competenceId:
+                payload.competenceId ?? baseQuestion?.competenceId ?? null,
+            isForSelfassessment:
+                payload.isForSelfassessment ??
+                baseQuestion?.isForSelfassessment ??
+                false,
+        });
+ 
+        return this.questions.create(question);
+    }
+ 
+    async listQuestions(query: QuestionSearchQuery): Promise<QuestionDomain[]> {
+        return this.questions.search(query);
+    }
+ 
+    async deleteQuestion(id: number): Promise<void> {
+        const question = await this.questions.findById(id);
+ 
+        if (!question) {
+            throw new NotFoundException(
+                'Question with id ' + id + ' not found',
+            );
+        }
+ 
+        if (question.cycleId) {
+            const cycle = await this.cycles.getById(question.cycleId);
+            Eif (
+                cycle.stage !== CycleStage.NEW &&
+                cycle.stage !== CycleStage.ACTIVE
+            ) {
+                throw new BadRequestException(
+                    'Question #' +
+                        id +
+                        ' cannot be deleted from cycle that is not active. Current stage: ' +
+                        cycle.stage,
+                );
+            }
+        }
+ 
+        await this.questions.deleteById(id);
+    }
+ 
+    async attachQuestion(
+        payload: AddQuestionToReviewPayload,
+    ): Promise<ReviewQuestionRelationDomain> {
+        const review = await this.getById(payload.reviewId);
+ 
+        if (review.stage !== ReviewStage.NEW) {
+            throw new BadRequestException(
+                'Review #' +
+                    review.id +
+                    ' must be not started to be assigned to question. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        const questionTemplate = await this.questionTemplates.getById(
+            payload.questionTemplateId,
+        );
+        const competence = await this.competences.getById(
+            questionTemplate.competenceId,
+        );
+ 
+        // The relation table FK requires a real `feedback360_questions` row.
+        // Reuse an existing snapshot for this (cycle, template) tuple if present,
+        // otherwise create a new feedback360 Question from the library template.
+        const existingQuestions = await this.questions.search({
+            cycleId: review.cycleId ?? undefined,
+            questionTemplateId: questionTemplate.id!,
+        });
+        const reviewQuestion =
+            existingQuestions[0] ??
+            (await this.questions.create(
+                QuestionDomain.create({
+                    cycleId: review.cycleId ?? null,
+                    questionTemplateId: questionTemplate.id!,
+                    title: questionTemplate.title,
+                    answerType: questionTemplate.answerType,
+                    competenceId: questionTemplate.competenceId,
+                    isForSelfassessment: questionTemplate.isForSelfassessment,
+                }),
+            ));
+ 
+        const relation = ReviewQuestionRelationDomain.create({
+            reviewId: payload.reviewId,
+            questionId: reviewQuestion.id!,
+            questionTitle: questionTemplate.title,
+            answerType: questionTemplate.answerType,
+            competenceId: questionTemplate.competenceId,
+            competenceTitle: competence.title,
+            competenceCode: competence.code,
+            competenceDescription: competence.description,
+            isForSelfassessment: questionTemplate.isForSelfassessment,
+        });
+ 
+        return this.questionRelations.link(relation);
+    }
+ 
+    async listQuestionRelations(
+        reviewId: number,
+        query: ReviewQuestionRelationSearchQuery,
+        actor?: UserDomain,
+    ): Promise<ReviewQuestionRelationDomain[]> {
+        await this.getById(reviewId, actor);
+        return this.questionRelations.listByReview(reviewId, query);
+    }
+ 
+    async detachQuestion(reviewId: number, questionId: number): Promise<void> {
+        const review = await this.getById(reviewId);
+ 
+        if (review.stage !== ReviewStage.NEW) {
+            throw new BadRequestException(
+                'Question #' +
+                    questionId +
+                    ' cannot be unassigned from review that is already started. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        await this.questionRelations.unlink(reviewId, questionId);
+    }
+ 
+    async addAnswer(
+        payload: CreateAnswerPayload,
+        actor: UserDomain,
+    ): Promise<AnswerDomain> {
+        const review = await this.getById(payload.reviewId, actor);
+ 
+        if (
+            review.stage !== ReviewStage.IN_PROGRESS &&
+            review.stage !== ReviewStage.SELF_ASSESSMENT
+        ) {
+            throw new BadRequestException(
+                'Review #' +
+                    review.id +
+                    ' must be in progress to add answer. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        const answer = AnswerDomain.create({
+            reviewId: payload.reviewId,
+            questionId: payload.questionId,
+            respondentCategory: payload.respondentCategory,
+            answerType: payload.answerType,
+            numericalValue: payload.numericalValue ?? null,
+            textValue: payload.textValue ?? null,
+        });
+ 
+        return await this.answers.create(answer);
+    }
+ 
+    async listAnswers(
+        reviewId: number,
+        respondentCategory?: RespondentCategory,
+        actor?: UserDomain,
+    ): Promise<AnswerDomain[]> {
+        const review = await this.getById(reviewId);
+        await this.checkAccessToReviewAnswers(review, actor);
+ 
+        return this.answers.list({ reviewId: reviewId, respondentCategory });
+    }
+ 
+    async countReviewAnswers(
+        reviewId: number,
+        actor?: UserDomain,
+    ): Promise<number> {
+        const review = await this.getById(reviewId);
+        await this.checkAccessToReviewAnswers(review, actor);
+ 
+        const counts =
+            await this.answers.getAnswersCountByRespondentCategories(reviewId);
+ 
+        return counts.reduce((acc, c) => acc + c.answers, 0);
+    }
+ 
+    async addRespondent(
+        payload: AddRespondentPayload,
+    ): Promise<RespondentDomain> {
+        const review = await this.getById(payload.reviewId);
+ 
+        if (
+            review.stage !== ReviewStage.NEW &&
+            review.stage !== ReviewStage.IN_PROGRESS &&
+            review.stage !== ReviewStage.WAITING_TO_START &&
+            review.stage !== ReviewStage.SELF_ASSESSMENT
+        ) {
+            throw new BadRequestException(
+                'Respondent ' +
+                    payload.fullName +
+                    ' cannot be added to review #' +
+                    review.id +
+                    ' that is already finished or cancelled. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        const relation = RespondentDomain.create({
+            reviewId: payload.reviewId,
+            respondentId: payload.respondentId,
+            fullName: payload.fullName,
+            category: payload.category,
+            responseStatus: payload.responseStatus,
+            respondentNote: payload.respondentNote,
+            hrNote: payload.hrNote,
+            positionId: payload.positionId,
+            positionTitle: payload.positionTitle,
+            teamId: payload.teamId,
+            teamTitle: payload.teamTitle,
+            invitedAt: payload.invitedAt,
+            canceledAt: payload.canceledAt,
+            respondedAt: payload.respondedAt,
+        });
+ 
+        return this.respondents.create(relation);
+    }
+ 
+    async updateRespondent(
+        relationId: number,
+        patch: UpdateRespondentPayload,
+        actor: UserDomain,
+    ): Promise<RespondentDomain> {
+        await this.checkAccessToUpdateResponseStatus(relationId, actor);
+ 
+        const relation = await this.respondents.getById(relationId);
+        const review = await this.getById(relation.reviewId);
+ 
+        if (
+            review.stage !== ReviewStage.IN_PROGRESS &&
+            review.stage !== ReviewStage.SELF_ASSESSMENT
+        ) {
+            throw new BadRequestException(
+                'Review #' +
+                    review.id +
+                    ' must be in progress to update response status. Current stage: ' +
+                    review.stage,
+            );
+        }
+ 
+        const responseStatus = patch.responseStatus;
+        patch.responseStatus = undefined;
+ 
+        const updated = await this.respondents.updateById(relationId, patch);
+ 
+        Eif (responseStatus) {
+            await this.changeRespondentStatus(
+                review.id!,
+                relationId,
+                responseStatus,
+                actor.id!,
+                actor.fullName,
+                RESPONSE_TRANSITION_REASONS.SYSTEM_AUTOMATED,
+            );
+        }
+ 
+        return updated;
+    }
+ 
+    /**
+     * Centralized method for response status transitions with validation
+     * @param reviewId ID of the review
+     * @param nextStatus Target status
+     * @param actorId User ID who initiated the change (0 for system)
+     * @param actorName Full name of the actor
+     * @param reason Optional reason for the transition
+     */
+    async changeRespondentStatus(
+        reviewId: number,
+        relationId: number,
+        nextStatus: ResponseStatus,
+        actorId: number = SYSTEM_ACTOR.ID,
+        actorName: string = SYSTEM_ACTOR.NAME,
+        reason: string = RESPONSE_TRANSITION_REASONS.SYSTEM_AUTOMATED,
+    ): Promise<void> {
+        const review = await this.getById(reviewId);
+        const respondent = await this.respondents.getById(relationId);
+        if (respondent.reviewId != reviewId) {
+            throw new BadRequestException(
+                `Respondent relation #${relationId} does not belong to review #${reviewId}`,
+            );
+        }
+        const currentStatus = respondent.responseStatus;
+ 
+        // Validate transition is allowed
+        const allowedTransitions = RESPONSE_STATUS_TRANSITIONS[currentStatus];
+        if (!allowedTransitions.includes(nextStatus)) {
+            throw new BadRequestException(
+                `Invalid response status transition from ${currentStatus} to ${nextStatus}`,
+            );
+        }
+ 
+        await this.respondents.updateById(relationId, {
+            responseStatus: nextStatus,
+        });
+ 
+        this.eventEmitter.emit(
+            'respondent.status.changed',
+            new RespondentStatusChangedEvent(
+                reviewId,
+                relationId,
+                currentStatus,
+                nextStatus,
+                actorId,
+                actorName,
+                reason,
+            ),
+        );
+    }
+ 
+    /**
+     * Checks if the actor has access to update the response status of a respondent
+     * as an admin or the respondent themselves.
+     * @param relationId The respondent identifier.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToUpdateResponseStatus(
+        relationId: number,
+        actor?: UserDomain,
+    ): Promise<void> {
+        Iif (!actor) return;
+ 
+        const isAdmin = actor?.roles?.includes(IdentityRole.ADMIN);
+ 
+        Iif (isAdmin) return;
+ 
+        const respondent = await this.respondents.getById(relationId);
+ 
+        if (respondent.respondentId === actor.id) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to update response status for review #' +
+                respondent.reviewId +
+                '.',
+        );
+    }
+ 
+    async listRespondents(
+        reviewId: number,
+        query: RespondentSearchQuery,
+        actor?: UserDomain,
+    ): Promise<RespondentDomain[]> {
+        const review = await this.getById(reviewId);
+ 
+        Iif (!this.hasAccessToReadReview(review, actor)) {
+            throw new ForbiddenException(
+                'You do not have permission to view review #' + review.id + '.',
+            );
+        }
+ 
+        return this.respondents.listByReview(reviewId, query);
+    }
+ 
+    async removeRespondent(id: number): Promise<void> {
+        await this.respondents.deleteById(id);
+    }
+ 
+    async addReviewer(payload: AddReviewerPayload): Promise<ReviewerDomain> {
+        await this.getById(payload.reviewId);
+        const relation = ReviewerDomain.create({
+            reviewId: payload.reviewId,
+            reviewerId: payload.reviewerId,
+            fullName: payload.fullName,
+            positionId: payload.positionId,
+            positionTitle: payload.positionTitle,
+            teamId: payload.teamId,
+            teamTitle: payload.teamTitle,
+        });
+        return this.reviewers.create(relation);
+    }
+ 
+    async listReviewers(
+        reviewId: number,
+        query: ReviewerSearchQuery,
+        actor: UserDomain,
+    ): Promise<ReviewerDomain[]> {
+        await this.checkAccessToReviewers(reviewId, actor);
+        return this.reviewers.listByReview(reviewId, query);
+    }
+ 
+    /**
+     * Checks if the actor has access to the reviewers of the review
+     * as an admin, hr, manager, ratee, respondent, or reviewer of that review.
+     * @param reviewId The review identifier.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToReviewers(
+        reviewId: number,
+        actor?: UserDomain,
+    ): Promise<void> {
+        Iif (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const review = await this.getById(reviewId);
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        Iif (isManagerOfReview || isRateeOfReview) return;
+ 
+        const respondents = await this.respondents.listByReview(review.id!, {
+            respondentId: actor.id,
+        });
+ 
+        Iif (respondents.length > 0) return;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        Iif (reviewers.length > 0) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view reviewers of review #' +
+                review.id +
+                '.',
+        );
+    }
+ 
+    async removeReviewer(id: number): Promise<void> {
+        await this.reviewers.deleteById(id);
+    }
+ 
+    async upsertClusterScore(
+        payload: UpsertClusterScorePayload,
+    ): Promise<ClusterScoreDomain> {
+        if (payload.cycleId) {
+            await this.cycles.getById(payload.cycleId);
+        }
+ 
+        const score = ClusterScoreDomain.create({
+            cycleId: payload.cycleId ?? null,
+            clusterId: payload.clusterId,
+            rateeId: payload.rateeId,
+            reviewId: payload.reviewId,
+            score: payload.score,
+            answersCount: payload.answersCount ?? 1,
+        });
+ 
+        return this.clusterScores.upsert(score);
+    }
+ 
+    async listClusterScores(
+        query: ClusterScoreSearchQuery,
+    ): Promise<ClusterScoreDomain[]> {
+        return this.clusterScores.list(query);
+    }
+ 
+    async getClusterScoreById(
+        id: number,
+    ): Promise<ClusterScoreWithRelationsDomain> {
+        return this.clusterScores.getById(id);
+    }
+ 
+    async getClusterScoreByCycleId(
+        cycleId: number,
+    ): Promise<ClusterScoreWithRelationsDomain[]> {
+        return this.clusterScores.getByCycleId(cycleId);
+    }
+ 
+    async removeClusterScore(id: number): Promise<void> {
+        await this.clusterScores.deleteById(id);
+    }
+ 
+    /**
+     * Centralized method for review stage transitions with validation and history tracking
+     * @param reviewId ID of the review
+     * @param nextStage Target stage
+     * @param actorId User ID who initiated the change (0 for system)
+     * @param actorName Full name of the actor
+     * @param reason Optional reason for the transition
+     */
+    async changeReviewStage(
+        reviewId: number,
+        nextStage: ReviewStage,
+        actorId: number = SYSTEM_ACTOR.ID,
+        actorName: string | null = SYSTEM_ACTOR.NAME,
+        reason?: string,
+    ): Promise<void> {
+        const review = await this.getById(reviewId);
+        const currentStage = review.stage;
+ 
+        // Validate transition is allowed
+        const allowedTransitions = REVIEW_STAGE_TRANSITIONS[currentStage];
+ 
+        if (!allowedTransitions.includes(nextStage)) {
+            throw new BadRequestException(
+                `Invalid review stage transition from ${currentStage} to ${nextStage}`,
+            );
+        }
+ 
+        // Perform update and history logging in a transaction
+        await this.prisma.$transaction(async (tx) => {
+            // Update review stage
+            await tx.review.update({
+                where: { id: reviewId },
+                data: { stage: nextStage },
+            });
+ 
+            // Create history record
+            const history = ReviewStageHistoryDomain.create({
+                reviewId,
+                fromStage: currentStage,
+                toStage: nextStage,
+                changedById: actorId || null,
+                changedByName: actorName,
+                reason,
+            });
+ 
+            await this.stageHistory.create(history);
+        });
+ 
+        // Emit event for listeners to react
+        this.eventEmitter.emit(
+            'review.stage.changed',
+            new ReviewStageChangedEvent(
+                reviewId,
+                currentStage,
+                nextStage,
+                actorId,
+                actorName,
+                reason,
+            ),
+        );
+    }
+ 
+    /**
+     * Get stage change history for a review
+     */
+    async getStageHistory(
+        reviewId: number,
+    ): Promise<ReviewStageHistoryDomain[]> {
+        return this.stageHistory.findByReviewId(reviewId);
+    }
+ 
+    /**
+     * REACTIVE TRIGGER: Checks if all respondents for a review have completed their responses.
+     * If yes, automatically transitions the review to the PREPARING_REPORT stage.
+     * @param reviewId The review identifier.
+     */
+    async completeReview(reviewId: number): Promise<void> {
+        await this.getById(reviewId);
+ 
+        // Get all respondents for this review
+        const respondents = await this.respondents.listByReview(reviewId, {});
+ 
+        // Check if there are any pending or in-progress respondents
+        const incompleteRespondents = respondents.filter(
+            (r) =>
+                r.responseStatus === ResponseStatus.PENDING ||
+                r.responseStatus === ResponseStatus.IN_PROGRESS,
+        );
+        const hasPendingResponses = incompleteRespondents.length > 0;
+ 
+        // If no pending responses, trigger report generation
+        if (!hasPendingResponses && respondents.length > 0) {
+            await this.changeReviewStage(
+                reviewId,
+                ReviewStage.FINISHED,
+                SYSTEM_ACTOR.ID,
+                SYSTEM_ACTOR.NAME,
+                REVIEW_TRANSITION_REASONS.ALL_RESPONSES_COLLECTED,
+            );
+        } else {
+            throw new BadRequestException(
+                'All responses must be collected to finish review #' +
+                    reviewId +
+                    '. Incomplete responses count: ' +
+                    incompleteRespondents.length,
+            );
+        }
+    }
+ 
+    /**
+     * MANUAL TRIGGER: HR or ADMIN force-finishes a review
+     * Transitions review to PREPARING_REPORT regardless of pending responses
+     */
+    async forceFinishReview(
+        reviewId: number,
+        actorId: number,
+        actorName: string,
+    ): Promise<void> {
+        await this.changeReviewStage(
+            reviewId,
+            ReviewStage.FINISHED,
+            actorId,
+            actorName,
+            REVIEW_TRANSITION_REASONS.FORCE_FINISH,
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/identity/application/events/index.html b/apps/api/test/unit/coverage/lcov-report/identity/application/events/index.html new file mode 100644 index 00000000..cb44c597 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/identity/application/events/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for identity/application/events + + + + + + + + + +
+
+

All files identity/application/events

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
user-created.event.ts +
+
100%3/3100%0/0100%1/1100%3/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/identity/application/events/user-created.event.ts.html b/apps/api/test/unit/coverage/lcov-report/identity/application/events/user-created.event.ts.html new file mode 100644 index 00000000..104ac621 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/identity/application/events/user-created.event.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for identity/application/events/user-created.event.ts + + + + + + + + + +
+
+

All files / identity/application/events user-created.event.ts

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +  +  +18x +  +7x +7x +  +  + 
/**
+ * Domain event emitted when a new user is created via the identity
+ * service (HR-driven flow). Listeners (e.g. notifications) can react
+ * by sending a welcome email.
+ */
+export class UserCreatedEvent {
+    constructor(
+        public readonly userId: number,
+        public readonly email: string,
+    ) {}
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/identity/application/ports/index.html b/apps/api/test/unit/coverage/lcov-report/identity/application/ports/index.html new file mode 100644 index 00000000..15f8b945 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/identity/application/ports/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for identity/application/ports + + + + + + + + + +
+
+

All files identity/application/ports

+
+ +
+ 100% + Statements + 2/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 2/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
role.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
user.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/identity/application/ports/role.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/identity/application/ports/role.repository.port.ts.html new file mode 100644 index 00000000..126a3f8e --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/identity/application/ports/role.repository.port.ts.html @@ -0,0 +1,112 @@ + + + + + + Code coverage report for identity/application/ports/role.repository.port.ts + + + + + + + + + +
+
+

All files / identity/application/ports role.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10  +  +  +17x +  +  +  +  +  + 
import { IdentityRole } from '@intra/shared-kernel';
+import { RoleDomain } from '../../domain/role.domain';
+ 
+export const IDENTITY_ROLE_REPOSITORY = Symbol('IDENTITY.ROLE_REPOSITORY');
+ 
+export interface RoleRepositoryPort {
+    getAll(): Promise<RoleDomain[]>;
+    findByCodes(codes: IdentityRole[]): Promise<RoleDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/identity/application/ports/user.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/identity/application/ports/user.repository.port.ts.html new file mode 100644 index 00000000..2d643bc0 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/identity/application/ports/user.repository.port.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for identity/application/ports/user.repository.port.ts + + + + + + + + + +
+
+

All files / identity/application/ports user.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +  +  +  +  +  +24x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    IdentityRole,
+    UpdateUserPayload,
+    UserSearchQuery,
+} from '@intra/shared-kernel';
+import { UserDomain } from '../../domain/user.domain';
+ 
+export const IDENTITY_USER_REPOSITORY = Symbol('IDENTITY.USER_REPOSITORY');
+ 
+export interface UserRepositoryPort {
+    create(user: UserDomain): Promise<UserDomain>;
+    findById(
+        id: number,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain | null>;
+    findByEmail(
+        email: string,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain | null>;
+    search(query: UserSearchQuery): Promise<UserDomain[]>;
+    updateById(id: number, patch: UpdateUserPayload): Promise<UserDomain>;
+    deleteById(id: number): Promise<void>;
+    replaceRoles(userId: number, roles: IdentityRole[]): Promise<UserDomain>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/identity/application/services/identity-role.service.ts.html b/apps/api/test/unit/coverage/lcov-report/identity/application/services/identity-role.service.ts.html new file mode 100644 index 00000000..96488f53 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/identity/application/services/identity-role.service.ts.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for identity/application/services/identity-role.service.ts + + + + + + + + + +
+
+

All files / identity/application/services identity-role.service.ts

+
+ +
+ 100% + Statements + 8/8 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22  +2x +  +  +2x +  +  +2x +  +  +2x +  +  +  +1x +  +  +  +1x +  +  + 
import { IdentityRole } from '@intra/shared-kernel';
+import { Inject, Injectable } from '@nestjs/common';
+import { RoleDomain } from '../../domain/role.domain';
+import type { RoleRepositoryPort } from '../ports/role.repository.port';
+import { IDENTITY_ROLE_REPOSITORY } from '../ports/role.repository.port';
+ 
+@Injectable()
+export class IdentityRoleService {
+    constructor(
+        @Inject(IDENTITY_ROLE_REPOSITORY)
+        private readonly roles: RoleRepositoryPort,
+    ) {}
+ 
+    async list(): Promise<RoleDomain[]> {
+        return this.roles.getAll();
+    }
+ 
+    async getByCodes(codes: IdentityRole[]): Promise<RoleDomain[]> {
+        return this.roles.findByCodes(codes);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/identity/application/services/identity-user.service.ts.html b/apps/api/test/unit/coverage/lcov-report/identity/application/services/identity-user.service.ts.html new file mode 100644 index 00000000..9ceff88e --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/identity/application/services/identity-user.service.ts.html @@ -0,0 +1,682 @@ + + + + + + Code coverage report for identity/application/services/identity-user.service.ts + + + + + + + + + +
+
+

All files / identity/application/services identity-user.service.ts

+
+ +
+ 100% + Statements + 54/54 +
+ + +
+ 93.02% + Branches + 40/43 +
+ + +
+ 100% + Functions + 12/12 +
+ + +
+ 100% + Lines + 49/49 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +20016x +  +  +  +  +  +  +16x +  +  +  +  +  +16x +16x +16x +  +16x +  +16x +  +  +  +  +16x +  +  +19x +  +19x +19x +  +  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +5x +1x +  +  +  +  +4x +  +4x +3x +  +  +  +  +  +4x +  +  +  +1x +  +  +  +  +  +  +10x +10x +7x +  +  +  +  +  +  +1x +  +  +  +3x +  +2x +  +  +  +2x +  +  +  +  +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +1x +  +  +  +  +  +  +2x +  +2x +2x +2x +1x +2x +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +2x +  +  +  +2x +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +1x +1x +  +  +  +  +  +1x +  +  +  +  +  +  +  +6x +  +  +6x +  +  + 
import {
+    CreateUserPayload,
+    IdentityRole,
+    IdentityStatus,
+    UpdateUserPayload,
+    UserSearchQuery,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { EventEmitter2 } from '@nestjs/event-emitter';
+import { UserDomain } from '../../domain/user.domain';
+import { UserCreatedEvent } from '../events/user-created.event';
+import type { RoleRepositoryPort } from '../ports/role.repository.port';
+import { IDENTITY_ROLE_REPOSITORY } from '../ports/role.repository.port';
+import type { UserRepositoryPort } from '../ports/user.repository.port';
+import { IDENTITY_USER_REPOSITORY } from '../ports/user.repository.port';
+ 
+export type UpdateUserCommand = Partial<Omit<CreateUserPayload, 'email'>>;
+ 
+@Injectable()
+export class IdentityUserService {
+    constructor(
+        @Inject(IDENTITY_USER_REPOSITORY)
+        private readonly users: UserRepositoryPort,
+        @Inject(IDENTITY_ROLE_REPOSITORY)
+        private readonly roles: RoleRepositoryPort,
+        private readonly eventEmitter: EventEmitter2,
+    ) {}
+ 
+    async create(payload: CreateUserPayload): Promise<UserDomain> {
+        const user = UserDomain.create({
+            firstName: payload.firstName,
+            secondName: payload.secondName,
+            lastName: payload.lastName,
+            fullName:
+                payload.fullName ??
+                this.buildFullName(
+                    payload.lastName,
+                    payload.firstName,
+                    payload.secondName,
+                ),
+            email: payload.email,
+            avatarUrl: payload.avatarUrl,
+            status: payload.status ?? IdentityStatus.ACTIVE,
+            positionId: payload.positionId ?? null,
+            teamId: payload.teamId ?? null,
+            managerId: payload.managerId ?? null,
+            roles: payload.roles ?? [],
+        });
+ 
+        const existing = await this.users.findByEmail(payload.email, {
+            withRoles: true,
+        });
+ 
+        if (existing) {
+            throw new BadRequestException(
+                `User with email ${payload.email} already exists`,
+            );
+        }
+ 
+        const created = await this.users.create(user);
+ 
+        if (created.id) {
+            this.eventEmitter.emit(
+                'user.created',
+                new UserCreatedEvent(created.id, created.email),
+            );
+        }
+ 
+        return created;
+    }
+ 
+    async search(query: UserSearchQuery): Promise<UserDomain[]> {
+        return this.users.search(query);
+    }
+ 
+    async getById(
+        id: number,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain> {
+        const user = await this.users.findById(id, opts);
+        if (!user) throw new NotFoundException('User not found');
+        return user;
+    }
+ 
+    async findByEmail(
+        email: string,
+        opts?: { withRoles?: boolean },
+    ): Promise<UserDomain | null> {
+        return this.users.findByEmail(email, opts);
+    }
+ 
+    async update(id: number, patch: UpdateUserCommand): Promise<UserDomain> {
+        const current = await this.getById(id);
+        const shouldUpdateFullName =
+            patch.firstName !== undefined ||
+            patch.secondName !== undefined ||
+            patch.lastName !== undefined;
+ 
+        const fullName = shouldUpdateFullName
+            ? this.buildFullName(
+                  patch.lastName ?? current.lastName,
+                  patch.firstName ?? current.firstName,
+                  patch.secondName ?? current.secondName,
+              )
+            : undefined;
+ 
+        const payload: UpdateUserPayload = {
+            ...patch,
+            ...(fullName !== undefined ? { fullName } : {}),
+        };
+ 
+        return this.users.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        await this.users.deleteById(id);
+    }
+ 
+    async replaceRoles(
+        userId: number,
+        roles: IdentityRole[],
+    ): Promise<UserDomain> {
+        await this.getById(userId);
+ 
+        const uniqueRoles = Array.from(new Set(roles));
+        const found = await this.roles.findByCodes(uniqueRoles);
+        if (found.length !== uniqueRoles.length) {
+            const foundCodes = new Set(found.map((r) => r.code));
+            const missing = uniqueRoles.filter((c) => !foundCodes.has(c));
+            throw new BadRequestException(
+                `Unknown roles: ${missing.join(', ')}`,
+            );
+        }
+ 
+        return this.users.replaceRoles(userId, uniqueRoles);
+    }
+ 
+    async upsertExternalUser(payload: {
+        email: string;
+        firstName: string;
+        lastName: string;
+        secondName: string | undefined;
+        avatarUrl?: string;
+    }): Promise<UserDomain> {
+        const existing = await this.users.findByEmail(payload.email, {
+            withRoles: true,
+        });
+ 
+        if (existing) {
+            return existing;
+        }
+ 
+        const createdUser = UserDomain.create({
+            firstName: payload.firstName,
+            secondName: payload.secondName,
+            lastName: payload.lastName,
+            fullName: this.buildFullName(
+                payload.lastName,
+                payload.firstName,
+                payload.secondName,
+            ),
+            email: payload.email,
+            avatarUrl: payload.avatarUrl,
+            status: IdentityStatus.ACTIVE,
+            positionId: null,
+            teamId: null,
+            managerId: null,
+            roles: [IdentityRole.EMPLOYEE],
+        });
+ 
+        const persisted = await this.users.create(createdUser);
+ 
+        Eif (persisted.id) {
+            this.eventEmitter.emit(
+                'user.created',
+                new UserCreatedEvent(persisted.id, persisted.email),
+            );
+        }
+ 
+        return persisted;
+    }
+ 
+    private buildFullName(
+        lastName: string,
+        firstName: string,
+        secondName: string | undefined,
+    ): string {
+        const parts = [lastName, firstName, secondName ?? undefined].filter(
+            Boolean,
+        );
+        return parts.join(' ');
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/identity/application/services/index.html b/apps/api/test/unit/coverage/lcov-report/identity/application/services/index.html new file mode 100644 index 00000000..aefb34d7 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/identity/application/services/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for identity/application/services + + + + + + + + + +
+
+

All files identity/application/services

+
+ +
+ 100% + Statements + 62/62 +
+ + +
+ 93.02% + Branches + 40/43 +
+ + +
+ 100% + Functions + 15/15 +
+ + +
+ 100% + Lines + 55/55 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
identity-role.service.ts +
+
100%8/8100%0/0100%3/3100%6/6
identity-user.service.ts +
+
100%54/5493.02%40/43100%12/12100%49/49
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/index.html b/apps/api/test/unit/coverage/lcov-report/index.html new file mode 100644 index 00000000..45bda8e0 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/index.html @@ -0,0 +1,371 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 96.77% + Statements + 1891/1954 +
+ + +
+ 74.47% + Branches + 1205/1618 +
+ + +
+ 96.09% + Functions + 320/333 +
+ + +
+ 97.54% + Lines + 1785/1830 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
feedback360/application/constants +
+
100%9/9100%0/0100%0/0100%9/9
feedback360/application/events +
+
100%36/36100%0/0100%7/7100%36/36
feedback360/application/listeners +
+
100%122/12278.07%89/114100%11/11100%114/114
feedback360/application/ports +
+
100%11/11100%0/0100%0/0100%11/11
feedback360/application/services +
+
97.17%447/46078.92%352/446100%75/7599.3%426/429
identity/application/events +
+
100%3/3100%0/0100%1/1100%3/3
identity/application/ports +
+
100%2/2100%0/0100%0/0100%2/2
identity/application/services +
+
100%62/6293.02%40/43100%15/15100%55/55
library/application/ports +
+
100%6/6100%0/0100%0/0100%6/6
library/application/services +
+
100%157/15775%96/128100%38/38100%143/143
notifications/application/listeners +
+
100%65/6572.72%40/55100%6/6100%59/59
notifications/application/ports +
+
100%2/2100%0/0100%0/0100%2/2
notifications/application/services +
+
95.16%118/12470.83%68/96100%9/995.08%116/122
organisation/application/ports +
+
100%3/3100%0/0100%0/0100%3/3
organisation/application/services +
+
100%98/9888.63%39/44100%25/25100%88/88
reporting/application/listeners +
+
94.23%49/5271.42%30/4280%4/593.75%45/48
reporting/application/ports +
+
100%7/7100%0/0100%0/0100%7/7
reporting/application/services +
+
94.42%694/73569.38%451/65091.48%129/14195.23%660/693
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/ports/cluster.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/ports/cluster.repository.port.ts.html new file mode 100644 index 00000000..481bf7aa --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/ports/cluster.repository.port.ts.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for library/application/ports/cluster.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports cluster.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +  +  +11x +  +  +  +  +  +  +  +  + 
import { ClusterSearchQuery, UpdateClusterPayload } from '@intra/shared-kernel';
+import { ClusterDomain } from '../../domain/cluster.domain';
+ 
+export const CLUSTER_REPOSITORY = Symbol('LIBRARY.CLUSTER_REPOSITORY');
+ 
+export interface ClusterRepositoryPort {
+    create(cluster: ClusterDomain): Promise<ClusterDomain>;
+    findById(id: number): Promise<ClusterDomain | null>;
+    search(query: ClusterSearchQuery): Promise<ClusterDomain[]>;
+    updateById(id: number, patch: UpdateClusterPayload): Promise<ClusterDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/ports/competence-question-template-relation.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/ports/competence-question-template-relation.repository.port.ts.html new file mode 100644 index 00000000..39ce7f66 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/ports/competence-question-template-relation.repository.port.ts.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for library/application/ports/competence-question-template-relation.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports competence-question-template-relation.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26  +  +16x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { CompetenceQuestionTemplateRelationDomain } from '../../domain/competence-question-template-relation.domain';
+ 
+export const COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY = Symbol(
+    'LIBRARY.COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY',
+);
+ 
+export interface CompetenceQuestionTemplateRelationRepositoryPort {
+    link(
+        competenceId: number,
+        questionTemplateId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain>;
+    unlink(competenceId: number, questionTemplateId: number): Promise<void>;
+    listByCompetence(
+        competenceId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]>;
+    listByQuestionTemplate(
+        questionTemplateId: number,
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]>;
+    replaceForCompetence(
+        competenceId: number,
+        questionTemplateIds: number[],
+    ): Promise<CompetenceQuestionTemplateRelationDomain[]>;
+    deleteAllForQuestionTemplate(questionTemplateId: number): Promise<void>;
+    deleteAllForCompetence(competenceId: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/ports/competence.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/ports/competence.repository.port.ts.html new file mode 100644 index 00000000..d530a1da --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/ports/competence.repository.port.ts.html @@ -0,0 +1,139 @@ + + + + + + Code coverage report for library/application/ports/competence.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports competence.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19  +  +  +  +  +  +21x +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    CompetenceSearchQuery,
+    UpdateCompetencePayload,
+} from '@intra/shared-kernel';
+import { CompetenceDomain } from '../../domain/competence.domain';
+ 
+export const COMPETENCE_REPOSITORY = Symbol('LIBRARY.COMPETENCE_REPOSITORY');
+ 
+export interface CompetenceRepositoryPort {
+    create(competence: CompetenceDomain): Promise<CompetenceDomain>;
+    findById(id: number): Promise<CompetenceDomain | null>;
+    search(query: CompetenceSearchQuery): Promise<CompetenceDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdateCompetencePayload,
+    ): Promise<CompetenceDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/ports/index.html b/apps/api/test/unit/coverage/lcov-report/library/application/ports/index.html new file mode 100644 index 00000000..b3d1c202 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/ports/index.html @@ -0,0 +1,191 @@ + + + + + + Code coverage report for library/application/ports + + + + + + + + + +
+
+

All files library/application/ports

+
+ +
+ 100% + Statements + 6/6 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cluster.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
competence-question-template-relation.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
competence.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
position-competence-relation.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
position-question-template-relation.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
question-template.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/ports/position-competence-relation.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/ports/position-competence-relation.repository.port.ts.html new file mode 100644 index 00000000..0f839764 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/ports/position-competence-relation.repository.port.ts.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for library/application/ports/position-competence-relation.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports position-competence-relation.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +19x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { PositionCompetenceRelationDomain } from '../../domain/position-competence-relation.domain';
+ 
+export const POSITION_COMPETENCE_RELATION_REPOSITORY = Symbol(
+    'LIBRARY.POSITION_COMPETENCE_RELATION_REPOSITORY',
+);
+ 
+export interface PositionCompetenceRelationRepositoryPort {
+    link(
+        competenceId: number,
+        positionId: number,
+    ): Promise<PositionCompetenceRelationDomain>;
+    unlink(competenceId: number, positionId: number): Promise<void>;
+    listByCompetence(
+        competenceId: number,
+    ): Promise<PositionCompetenceRelationDomain[]>;
+    listByPosition(
+        positionId: number,
+    ): Promise<PositionCompetenceRelationDomain[]>;
+    replaceForCompetence(
+        competenceId: number,
+        positionIds: number[],
+    ): Promise<PositionCompetenceRelationDomain[]>;
+    deleteAllForCompetence(competenceId: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/ports/position-question-template-relation.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/ports/position-question-template-relation.repository.port.ts.html new file mode 100644 index 00000000..116cfa2f --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/ports/position-question-template-relation.repository.port.ts.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for library/application/ports/position-question-template-relation.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports position-question-template-relation.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22  +  +9x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { PositionQuestionTemplateRelationDomain } from '../../domain/position-question-template-relation.domain';
+ 
+export const POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY = Symbol(
+    'LIBRARY.POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY',
+);
+ 
+export interface PositionQuestionTemplateRelationRepositoryPort {
+    link(
+        questionId: number,
+        positionId: number,
+    ): Promise<PositionQuestionTemplateRelationDomain>;
+    unlink(questionId: number, positionId: number): Promise<void>;
+    listByQuestion(
+        questionId: number,
+    ): Promise<PositionQuestionTemplateRelationDomain[]>;
+    replace(
+        questionId: number,
+        positionIds: number[],
+    ): Promise<PositionQuestionTemplateRelationDomain[]>;
+    deleteAllForQuestionTemplate(questionTemplateId: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/ports/question-template.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/ports/question-template.repository.port.ts.html new file mode 100644 index 00000000..da70d3d5 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/ports/question-template.repository.port.ts.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for library/application/ports/question-template.repository.port.ts + + + + + + + + + +
+
+

All files / library/application/ports question-template.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23  +  +  +  +  +  +16x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    QuestionTemplateSearchQuery,
+    UpdateQuestionTemplatePayload,
+} from '@intra/shared-kernel';
+import { QuestionTemplateDomain } from '../../domain/question-template.domain';
+ 
+export const QUESTION_TEMPLATE_REPOSITORY = Symbol(
+    'LIBRARY.QUESTION_TEMPLATE_REPOSITORY',
+);
+ 
+export interface QuestionTemplateRepositoryPort {
+    create(question: QuestionTemplateDomain): Promise<QuestionTemplateDomain>;
+    findById(id: number): Promise<QuestionTemplateDomain | null>;
+    search(
+        query: QuestionTemplateSearchQuery,
+    ): Promise<QuestionTemplateDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdateQuestionTemplatePayload,
+    ): Promise<QuestionTemplateDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/services/cluster.service.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/services/cluster.service.ts.html new file mode 100644 index 00000000..3b5e4c23 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/services/cluster.service.ts.html @@ -0,0 +1,382 @@ + + + + + + Code coverage report for library/application/services/cluster.service.ts + + + + + + + + + +
+
+

All files / library/application/services cluster.service.ts

+
+ +
+ 100% + Statements + 31/31 +
+ + +
+ 79.41% + Branches + 27/34 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 28/28 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100  +  +  +  +  +5x +  +  +  +  +  +5x +5x +  +  +  +5x +  +  +5x +  +  +12x +12x +  +  +  +2x +1x +  +1x +  +  +  +  +  +  +  +1x +  +  +  +1x +  +  +  +9x +9x +6x +  +  +  +  +  +  +5x +  +4x +1x +  +  +4x +1x +1x +1x +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +2x +1x +  +  +  +3x +2x +  +  +  +  +  + 
import {
+    ClusterSearchQuery,
+    CreateClusterPayload,
+    UpdateClusterPayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { ClusterDomain } from '../../domain/cluster.domain';
+import {
+    CLUSTER_REPOSITORY,
+    ClusterRepositoryPort,
+} from '../ports/cluster.repository.port';
+import { CompetenceService } from './competence.service';
+ 
+@Injectable()
+export class ClusterService {
+    constructor(
+        @Inject(CLUSTER_REPOSITORY)
+        private readonly clusters: ClusterRepositoryPort,
+        private readonly competences: CompetenceService,
+    ) {}
+ 
+    async create(payload: CreateClusterPayload): Promise<ClusterDomain> {
+        await this.ensureBounds(payload.lowerBound, payload.upperBound);
+        await this.competences.getById(payload.competenceId);
+ 
+        const cluster = ClusterDomain.create({
+            competenceId: payload.competenceId,
+            lowerBound: payload.lowerBound,
+            upperBound: payload.upperBound,
+            title: payload.title,
+            description: payload.description,
+        });
+ 
+        return this.clusters.create(cluster);
+    }
+ 
+    async search(query: ClusterSearchQuery): Promise<ClusterDomain[]> {
+        return this.clusters.search(query);
+    }
+ 
+    async getById(id: number): Promise<ClusterDomain> {
+        const cluster = await this.clusters.findById(id);
+        if (!cluster) throw new NotFoundException('Cluster not found');
+        return cluster;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateClusterPayload,
+    ): Promise<ClusterDomain> {
+        const current = await this.getById(id);
+ 
+        if (patch.competenceId && patch.competenceId !== current.competenceId) {
+            await this.competences.getById(patch.competenceId);
+        }
+ 
+        if (patch.lowerBound !== undefined || patch.upperBound !== undefined) {
+            const lower = patch.lowerBound ?? current.lowerBound;
+            const upper = patch.upperBound ?? current.upperBound;
+            await this.ensureBounds(lower, upper);
+        }
+ 
+        const payload: UpdateClusterPayload = {
+            ...(patch.competenceId !== undefined
+                ? { competenceId: patch.competenceId }
+                : {}),
+            ...(patch.lowerBound !== undefined
+                ? { lowerBound: patch.lowerBound }
+                : {}),
+            ...(patch.upperBound !== undefined
+                ? { upperBound: patch.upperBound }
+                : {}),
+            ...(patch.title !== undefined ? { title: patch.title } : {}),
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+        };
+ 
+        return this.clusters.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        await this.clusters.deleteById(id);
+    }
+ 
+    private async ensureBounds(lower: number, upper: number): Promise<void> {
+        if (lower > upper) {
+            throw new BadRequestException(
+                'Lower bound must be less than or equal to upper bound',
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/services/competence.service.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/services/competence.service.ts.html new file mode 100644 index 00000000..84eeeb62 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/services/competence.service.ts.html @@ -0,0 +1,601 @@ + + + + + + Code coverage report for library/application/services/competence.service.ts + + + + + + + + + +
+
+

All files / library/application/services competence.service.ts

+
+ +
+ 100% + Statements + 56/56 +
+ + +
+ 68.42% + Branches + 26/38 +
+ + +
+ 100% + Functions + 15/15 +
+ + +
+ 100% + Lines + 50/50 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173  +  +  +  +  +16x +  +  +  +  +  +16x +16x +16x +  +  +  +16x +  +  +  +16x +  +  +  +16x +  +  +  +  +  +16x +  +  +17x +  +17x +  +17x +  +17x +17x +  +  +  +2x +  +  +  +  +2x +  +  +  +1x +  +  +  +22x +22x +20x +  +  +  +  +  +  +2x +1x +  +  +  +  +  +  +1x +  +  +  +3x +3x +3x +3x +  +  +  +2x +  +  +  +  +  +  +  +  +  +1x +1x +  +  +1x +  +  +  +  +  +  +1x +1x +  +1x +1x +  +  +  +  +  +  +1x +1x +  +  +  +5x +  +5x +  +  +5x +  +  +  +  +  +  +2x +2x +  +1x +  +  +  +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +5x +  +5x +  +  +5x +  +  +  +  +  +2x +2x +  +  + 
import {
+    CompetenceSearchQuery,
+    CreateCompetencePayload,
+    UpdateCompetencePayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { PositionService } from 'src/contexts/organisation/application/services/position.service';
+import { CompetenceDomain } from '../../domain/competence.domain';
+import {
+    COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY,
+    CompetenceQuestionTemplateRelationRepositoryPort,
+} from '../ports/competence-question-template-relation.repository.port';
+import {
+    COMPETENCE_REPOSITORY,
+    CompetenceRepositoryPort,
+} from '../ports/competence.repository.port';
+import {
+    POSITION_COMPETENCE_RELATION_REPOSITORY,
+    PositionCompetenceRelationRepositoryPort,
+} from '../ports/position-competence-relation.repository.port';
+import {
+    QUESTION_TEMPLATE_REPOSITORY,
+    QuestionTemplateRepositoryPort,
+} from '../ports/question-template.repository.port';
+ 
+@Injectable()
+export class CompetenceService {
+    constructor(
+        @Inject(COMPETENCE_REPOSITORY)
+        private readonly competences: CompetenceRepositoryPort,
+        @Inject(QUESTION_TEMPLATE_REPOSITORY)
+        private readonly questionTemplates: QuestionTemplateRepositoryPort,
+        @Inject(POSITION_COMPETENCE_RELATION_REPOSITORY)
+        private readonly positionCompetenceRelations: PositionCompetenceRelationRepositoryPort,
+        @Inject(COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY)
+        private readonly competenceQuestionTemplateRelations: CompetenceQuestionTemplateRelationRepositoryPort,
+        private readonly positions: PositionService,
+    ) {}
+ 
+    async create(payload: CreateCompetencePayload): Promise<CompetenceDomain> {
+        const competence = CompetenceDomain.create({
+            code: payload.code ?? null,
+            title: payload.title,
+            description: payload.description ?? null,
+        });
+        return this.competences.create(competence);
+    }
+ 
+    async search(query: CompetenceSearchQuery): Promise<CompetenceDomain[]> {
+        return this.competences.search(query);
+    }
+ 
+    async getById(id: number): Promise<CompetenceDomain> {
+        const competence = await this.competences.findById(id);
+        if (!competence) throw new NotFoundException('Competence not found');
+        return competence;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateCompetencePayload,
+    ): Promise<CompetenceDomain> {
+        await this.getById(id);
+        const payload: UpdateCompetencePayload = {
+            ...(patch.code !== undefined ? { code: patch.code } : {}),
+            ...(patch.title !== undefined ? { title: patch.title } : {}),
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+        };
+        return this.competences.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        const relatedQuestionTemplates = await this.listQuestionTemplates(id);
+        const relatedPositions = await this.listPositions(id);
+        if (
+            relatedQuestionTemplates.length > 0 ||
+            relatedPositions.length > 0
+        ) {
+            throw new BadRequestException(
+                'Competence #' +
+                    id +
+                    ' cannot be deleted. It has ' +
+                    relatedQuestionTemplates.length +
+                    ' question templates and ' +
+                    relatedPositions.length +
+                    ' positions assigned to it.',
+            );
+        }
+        await this.positionCompetenceRelations.deleteAllForCompetence(id);
+        await this.competenceQuestionTemplateRelations.deleteAllForCompetence(
+            id,
+        );
+        await this.competences.deleteById(id);
+    }
+ 
+    async attachPosition(
+        competenceId: number,
+        positionId: number,
+    ): Promise<number[]> {
+        await this.getById(competenceId);
+        await this.positions.getById(positionId);
+ 
+        await this.positionCompetenceRelations.link(competenceId, positionId);
+        return this.listPositions(competenceId);
+    }
+ 
+    async detachPosition(
+        competenceId: number,
+        positionId: number,
+    ): Promise<void> {
+        await this.getById(competenceId);
+        await this.positionCompetenceRelations.unlink(competenceId, positionId);
+    }
+ 
+    async listPositions(competenceId: number): Promise<number[]> {
+        await this.getById(competenceId);
+        const relations =
+            await this.positionCompetenceRelations.listByCompetence(
+                competenceId,
+            );
+        return relations.map((r) => r.positionId);
+    }
+ 
+    async attachQuestionTemplate(
+        competenceId: number,
+        questionTemplateId: number,
+    ): Promise<number[]> {
+        await this.getById(competenceId);
+        await this.ensureQuestionTemplateExists(questionTemplateId);
+ 
+        await this.competenceQuestionTemplateRelations.link(
+            competenceId,
+            questionTemplateId,
+        );
+        return this.listQuestionTemplates(competenceId);
+    }
+ 
+    async detachQuestionTemplate(
+        competenceId: number,
+        questionTemplateId: number,
+    ): Promise<void> {
+        await this.getById(competenceId);
+        await this.competenceQuestionTemplateRelations.unlink(
+            competenceId,
+            questionTemplateId,
+        );
+    }
+ 
+    async listQuestionTemplates(competenceId: number): Promise<number[]> {
+        await this.getById(competenceId);
+        const relations =
+            await this.competenceQuestionTemplateRelations.listByCompetence(
+                competenceId,
+            );
+        return relations.map((r) => r.questionTemplateId);
+    }
+ 
+    private async ensureQuestionTemplateExists(
+        questionTemplateId: number,
+    ): Promise<void> {
+        const found = await this.questionTemplates.findById(questionTemplateId);
+        if (!found) throw new NotFoundException('Question template not found');
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/services/index.html b/apps/api/test/unit/coverage/lcov-report/library/application/services/index.html new file mode 100644 index 00000000..5dc34822 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/services/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for library/application/services + + + + + + + + + +
+
+

All files library/application/services

+
+ +
+ 100% + Statements + 157/157 +
+ + +
+ 75% + Branches + 96/128 +
+ + +
+ 100% + Functions + 38/38 +
+ + +
+ 100% + Lines + 143/143 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cluster.service.ts +
+
100%31/3179.41%27/34100%7/7100%28/28
competence.service.ts +
+
100%56/5668.42%26/38100%15/15100%50/50
question-template.service.ts +
+
100%70/7076.78%43/56100%16/16100%65/65
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/library/application/services/question-template.service.ts.html b/apps/api/test/unit/coverage/lcov-report/library/application/services/question-template.service.ts.html new file mode 100644 index 00000000..4cad4fb6 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/library/application/services/question-template.service.ts.html @@ -0,0 +1,763 @@ + + + + + + Code coverage report for library/application/services/question-template.service.ts + + + + + + + + + +
+
+

All files / library/application/services question-template.service.ts

+
+ +
+ 100% + Statements + 70/70 +
+ + +
+ 76.78% + Branches + 43/56 +
+ + +
+ 100% + Functions + 16/16 +
+ + +
+ 100% + Lines + 65/65 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +2279x +  +  +  +  +  +9x +  +  +  +  +  +9x +  +  +  +9x +9x +9x +  +  +  +9x +  +  +  +9x +  +  +  +9x +  +  +9x +  +  +16x +  +16x +  +16x +16x +16x +  +16x +  +  +  +  +  +2x +  +2x +  +  +  +  +  +  +  +  +2x +  +2x +  +  +2x +1x +1x +  +  +  +  +  +2x +  +  +  +  +  +1x +  +  +  +20x +20x +1x +19x +  +  +  +  +  +  +3x +  +3x +1x +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +3x +1x +1x +1x +  +  +  +  +  +3x +  +  +  +2x +2x +  +  +2x +1x +  +  +  +  +  +  +  +1x +  +  +1x +  +  +1x +  +  +  +  +  +  +1x +1x +  +1x +  +  +  +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +2x +  +2x +  +  +3x +  +  +  +  +  +  +1x +1x +  +1x +  +  +  +1x +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +2x +  +2x +  +  +3x +  +  +  +4x +  +  + 
import {
+    CreateQuestionTemplatePayload,
+    QuestionTemplateSearchQuery,
+    QuestionTemplateStatus,
+    UpdateQuestionTemplatePayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import {
+    QUESTION_REPOSITORY,
+    QuestionRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/question.repository.port';
+import { PositionService } from 'src/contexts/organisation/application/services/position.service';
+import { QuestionTemplateDomain } from '../../domain/question-template.domain';
+import {
+    COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY,
+    CompetenceQuestionTemplateRelationRepositoryPort,
+} from '../ports/competence-question-template-relation.repository.port';
+import {
+    POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY,
+    PositionQuestionTemplateRelationRepositoryPort,
+} from '../ports/position-question-template-relation.repository.port';
+import {
+    QUESTION_TEMPLATE_REPOSITORY,
+    QuestionTemplateRepositoryPort,
+} from '../ports/question-template.repository.port';
+import { CompetenceService } from './competence.service';
+ 
+@Injectable()
+export class QuestionTemplateService {
+    constructor(
+        @Inject(QUESTION_TEMPLATE_REPOSITORY)
+        private readonly questionTemplates: QuestionTemplateRepositoryPort,
+        @Inject(POSITION_QUESTION_TEMPLATE_RELATION_REPOSITORY)
+        private readonly positionQuestionTemplateRelations: PositionQuestionTemplateRelationRepositoryPort,
+        @Inject(COMPETENCE_QUESTION_TEMPLATE_RELATION_REPOSITORY)
+        private readonly competenceQuestionTemplateRelations: CompetenceQuestionTemplateRelationRepositoryPort,
+        private readonly competences: CompetenceService,
+        private readonly positions: PositionService,
+        @Inject(QUESTION_REPOSITORY)
+        private readonly reviewQuestions: QuestionRepositoryPort,
+    ) {}
+ 
+    async create(
+        payload: CreateQuestionTemplatePayload,
+    ): Promise<QuestionTemplateDomain> {
+        await this.competences.getById(payload.competenceId);
+ 
+        const question = QuestionTemplateDomain.create({
+            competenceId: payload.competenceId,
+            title: payload.title,
+            answerType: payload.answerType,
+            isForSelfassessment: payload.isForSelfassessment ?? false,
+            status: payload.status ?? QuestionTemplateStatus.ACTIVE,
+            positionIds: payload.positionIds ?? [],
+        });
+ 
+        const created = await this.questionTemplates.create(question);
+ 
+        const positionIds = payload.positionIds
+            ? Array.from(new Set(payload.positionIds))
+            : [];
+        if (positionIds.length) {
+            await this.ensurePositionsExist(positionIds);
+            await this.positionQuestionTemplateRelations.replace(
+                created.id!,
+                positionIds,
+            );
+        }
+ 
+        return this.getById(created.id!);
+    }
+ 
+    async search(
+        query: QuestionTemplateSearchQuery,
+    ): Promise<QuestionTemplateDomain[]> {
+        return this.questionTemplates.search(query);
+    }
+ 
+    async getById(id: number): Promise<QuestionTemplateDomain> {
+        const question = await this.questionTemplates.findById(id);
+        if (!question)
+            throw new NotFoundException('Question template not found');
+        return question;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdateQuestionTemplatePayload,
+    ): Promise<QuestionTemplateDomain> {
+        const current = await this.getById(id);
+ 
+        if (patch.competenceId && patch.competenceId !== current.competenceId) {
+            await this.competences.getById(patch.competenceId);
+        }
+ 
+        const payload: UpdateQuestionTemplatePayload = {
+            ...(patch.competenceId !== undefined
+                ? { competenceId: patch.competenceId }
+                : {}),
+            ...(patch.title !== undefined ? { title: patch.title } : {}),
+            ...(patch.answerType !== undefined
+                ? { answerType: patch.answerType }
+                : {}),
+            ...(patch.isForSelfassessment !== undefined
+                ? { isForSelfassessment: patch.isForSelfassessment }
+                : {}),
+            ...(patch.status !== undefined ? { status: patch.status } : {}),
+            ...(patch.positionIds !== undefined
+                ? { positionIds: patch.positionIds }
+                : {}),
+        };
+ 
+        await this.questionTemplates.updateById(id, payload);
+ 
+        if (patch.positionIds) {
+            const uniquePositions = Array.from(new Set(patch.positionIds));
+            await this.ensurePositionsExist(uniquePositions);
+            await this.positionQuestionTemplateRelations.replace(
+                id,
+                uniquePositions,
+            );
+        }
+ 
+        return this.getById(id);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        const relatedQuestions = await this.reviewQuestions.search({
+            questionTemplateId: id,
+        });
+        if (relatedQuestions.length > 0) {
+            throw new BadRequestException(
+                'Question template #' +
+                    id +
+                    ' cannot be deleted. It has ' +
+                    relatedQuestions.length +
+                    ' review questions assigned to it.',
+            );
+        }
+        await this.positionQuestionTemplateRelations.deleteAllForQuestionTemplate(
+            id,
+        );
+        await this.competenceQuestionTemplateRelations.deleteAllForQuestionTemplate(
+            id,
+        );
+        await this.questionTemplates.deleteById(id);
+    }
+ 
+    async attachPosition(
+        questionId: number,
+        positionId: number,
+    ): Promise<number[]> {
+        await this.getById(questionId);
+        await this.positions.getById(positionId);
+ 
+        await this.positionQuestionTemplateRelations.link(
+            questionId,
+            positionId,
+        );
+        return this.listPositions(questionId);
+    }
+ 
+    async detachPosition(
+        questionId: number,
+        positionId: number,
+    ): Promise<void> {
+        await this.getById(questionId);
+        await this.positionQuestionTemplateRelations.unlink(
+            questionId,
+            positionId,
+        );
+    }
+ 
+    async listPositions(questionId: number): Promise<number[]> {
+        await this.getById(questionId);
+        const relations =
+            await this.positionQuestionTemplateRelations.listByQuestion(
+                questionId,
+            );
+        return relations.map((r) => r.positionId);
+    }
+ 
+    async attachCompetence(
+        questionId: number,
+        competenceId: number,
+    ): Promise<number[]> {
+        await this.getById(questionId);
+        await this.competences.getById(competenceId);
+ 
+        await this.competenceQuestionTemplateRelations.link(
+            competenceId,
+            questionId,
+        );
+        return this.listCompetences(questionId);
+    }
+ 
+    async detachCompetence(
+        questionId: number,
+        competenceId: number,
+    ): Promise<void> {
+        await this.getById(questionId);
+        await this.competenceQuestionTemplateRelations.unlink(
+            competenceId,
+            questionId,
+        );
+    }
+ 
+    async listCompetences(questionId: number): Promise<number[]> {
+        await this.getById(questionId);
+        const relations =
+            await this.competenceQuestionTemplateRelations.listByQuestionTemplate(
+                questionId,
+            );
+        return relations.map((r) => r.competenceId);
+    }
+ 
+    private async ensurePositionsExist(positionIds: number[]): Promise<void> {
+        await Promise.all(positionIds.map((id) => this.positions.getById(id)));
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/cycle-stage-notification.listener.ts.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/cycle-stage-notification.listener.ts.html new file mode 100644 index 00000000..bbb45fe8 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/cycle-stage-notification.listener.ts.html @@ -0,0 +1,214 @@ + + + + + + Code coverage report for notifications/application/listeners/cycle-stage-notification.listener.ts + + + + + + + + + +
+
+

All files / notifications/application/listeners cycle-stage-notification.listener.ts

+
+ +
+ 100% + Statements + 20/20 +
+ + +
+ 72.22% + Branches + 13/18 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 18/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +441x +1x +1x +1x +1x +  +  +1x +3x +  +  +3x +  +  +  +1x +3x +  +3x +3x +  +  +2x +  +  +1x +  +  +1x +  +  +1x +  +  +  +1x +1x +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+import { Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { CycleStageChangedEvent } from 'src/contexts/feedback360/application/events/cycle-stage-changed.event';
+import { ReviewEmailNotificationService } from '../services/review-email-notification.service';
+ 
+@Injectable()
+export class CycleStageNotificationListener {
+    private readonly logger = new Logger(CycleStageNotificationListener.name);
+ 
+    constructor(
+        private readonly notifications: ReviewEmailNotificationService,
+    ) {}
+ 
+    @OnEvent('cycle.stage.changed', { async: true })
+    async handle(event: CycleStageChangedEvent): Promise<void> {
+        const { cycleId, toStage } = event;
+ 
+        try {
+            switch (toStage) {
+                case CycleStage.PUBLISHED: {
+                    const sent =
+                        await this.notifications.notifyCycleStrategicReportReady(
+                            cycleId,
+                        );
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for cycle=${cycleId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                default:
+                    return;
+            }
+        } catch (error) {
+            const message =
+                error instanceof Error ? error.message : String(error);
+            this.logger.error(
+                `Failed to handle cycle.stage.changed for cycle=${cycleId} stage=${toStage}: ${message}`,
+                error instanceof Error ? error.stack : undefined,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/index.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/index.html new file mode 100644 index 00000000..33779a09 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for notifications/application/listeners + + + + + + + + + +
+
+

All files notifications/application/listeners

+
+ +
+ 100% + Statements + 65/65 +
+ + +
+ 72.72% + Branches + 40/55 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 59/59 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage-notification.listener.ts +
+
100%20/2072.22%13/18100%2/2100%18/18
review-stage-notification.listener.ts +
+
100%29/2976.19%16/21100%2/2100%27/27
user-created-notification.listener.ts +
+
100%16/1668.75%11/16100%2/2100%14/14
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/review-stage-notification.listener.ts.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/review-stage-notification.listener.ts.html new file mode 100644 index 00000000..31d00f21 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/review-stage-notification.listener.ts.html @@ -0,0 +1,286 @@ + + + + + + Code coverage report for notifications/application/listeners/review-stage-notification.listener.ts + + + + + + + + + +
+
+

All files / notifications/application/listeners review-stage-notification.listener.ts

+
+ +
+ 100% + Statements + 29/29 +
+ + +
+ 76.19% + Branches + 16/21 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 27/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +681x +1x +1x +1x +1x +  +  +1x +6x +  +  +6x +  +  +  +1x +6x +  +6x +6x +  +  +1x +  +  +1x +  +  +1x +  +  +  +2x +1x +  +  +1x +  +  +  +1x +1x +  +  +1x +  +  +  +1x +1x +  +  +1x +  +  +1x +  +  +  +1x +1x +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+import { Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { ReviewStageChangedEvent } from 'src/contexts/feedback360/application/events/review-stage-changed.event';
+import { ReviewEmailNotificationService } from '../services/review-email-notification.service';
+ 
+@Injectable()
+export class ReviewStageNotificationListener {
+    private readonly logger = new Logger(ReviewStageNotificationListener.name);
+ 
+    constructor(
+        private readonly notifications: ReviewEmailNotificationService,
+    ) {}
+ 
+    @OnEvent('review.stage.changed', { async: true })
+    async handle(event: ReviewStageChangedEvent): Promise<void> {
+        const { reviewId, toStage } = event;
+ 
+        try {
+            switch (toStage) {
+                case ReviewStage.SELF_ASSESSMENT: {
+                    const sent =
+                        await this.notifications.notifyRateeSelfAssessment(
+                            reviewId,
+                        );
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for review=${reviewId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                case ReviewStage.IN_PROGRESS: {
+                    const sent =
+                        await this.notifications.notifyRespondents(reviewId);
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for review=${reviewId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                case ReviewStage.PROCESSING_BY_HR: {
+                    const sent =
+                        await this.notifications.notifyHrReportReady(reviewId);
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for review=${reviewId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                case ReviewStage.PUBLISHED: {
+                    const sent =
+                        await this.notifications.notifyReviewers(reviewId);
+                    this.logger.log(
+                        `dispatched ${sent} email(s) for review=${reviewId} stage=${toStage}`,
+                    );
+                    return;
+                }
+                default:
+                    return;
+            }
+        } catch (error) {
+            const message =
+                error instanceof Error ? error.message : String(error);
+            this.logger.error(
+                `Failed to handle review.stage.changed for review=${reviewId} stage=${toStage}: ${message}`,
+                error instanceof Error ? error.stack : undefined,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/user-created-notification.listener.ts.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/user-created-notification.listener.ts.html new file mode 100644 index 00000000..c25b6dbe --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/listeners/user-created-notification.listener.ts.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for notifications/application/listeners/user-created-notification.listener.ts + + + + + + + + + +
+
+

All files / notifications/application/listeners user-created-notification.listener.ts

+
+ +
+ 100% + Statements + 16/16 +
+ + +
+ 68.75% + Branches + 11/16 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 14/14 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +331x +1x +1x +1x +  +  +1x +2x +  +  +2x +  +  +  +1x +2x +  +2x +2x +1x +  +  +  +  +1x +1x +  +  +  +  +  +  + 
import { Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { UserCreatedEvent } from 'src/contexts/identity/application/events/user-created.event';
+import { ReviewEmailNotificationService } from '../services/review-email-notification.service';
+ 
+@Injectable()
+export class UserCreatedNotificationListener {
+    private readonly logger = new Logger(UserCreatedNotificationListener.name);
+ 
+    constructor(
+        private readonly notifications: ReviewEmailNotificationService,
+    ) {}
+ 
+    @OnEvent('user.created', { async: true })
+    async handle(event: UserCreatedEvent): Promise<void> {
+        const { userId } = event;
+ 
+        try {
+            const sent = await this.notifications.notifyUserWelcome(userId);
+            this.logger.log(
+                `dispatched ${sent} welcome email(s) for user=${userId}`,
+            );
+        } catch (error) {
+            const message =
+                error instanceof Error ? error.message : String(error);
+            this.logger.error(
+                `Failed to handle user.created for user=${userId}: ${message}`,
+                error instanceof Error ? error.stack : undefined,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/index.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/index.html new file mode 100644 index 00000000..c5781064 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for notifications/application/ports + + + + + + + + + +
+
+

All files notifications/application/ports

+
+ +
+ 100% + Statements + 2/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 2/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
mailer.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
notification-log.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/mailer.port.ts.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/mailer.port.ts.html new file mode 100644 index 00000000..31822b5e --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/mailer.port.ts.html @@ -0,0 +1,163 @@ + + + + + + Code coverage report for notifications/application/ports/mailer.port.ts + + + + + + + + + +
+
+

All files / notifications/application/ports mailer.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +275x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
export const MAILER = Symbol('NOTIFICATIONS.MAILER');
+ 
+export type MailerAttachment = {
+    filename: string;
+    content: Buffer;
+    contentType?: string;
+    /**
+     * Content-ID for inline embedding. When set, the attachment is rendered
+     * inline by mail clients via `<img src="cid:<cid>">` references in the
+     * HTML body. Used here to embed the brand logo so Gmail (which strips
+     * data: URIs in <img>) still displays it.
+     */
+    cid?: string;
+};
+ 
+export type MailerSendOptions = {
+    to: string;
+    subject: string;
+    template: string;
+    context: Record<string, unknown>;
+    attachments?: MailerAttachment[];
+};
+ 
+export interface MailerPort {
+    sendMail(options: MailerSendOptions): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/notification-log.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/notification-log.repository.port.ts.html new file mode 100644 index 00000000..cdca7766 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/ports/notification-log.repository.port.ts.html @@ -0,0 +1,193 @@ + + + + + + Code coverage report for notifications/application/ports/notification-log.repository.port.ts + + + + + + + + + +
+
+

All files / notifications/application/ports notification-log.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { NotificationKind } from '../../domain/notification-kind.enum';
+import { NotificationLogDomain } from '../../domain/notification-log.domain';
+ 
+export const NOTIFICATION_LOG_REPOSITORY = Symbol(
+    'NOTIFICATIONS.NOTIFICATION_LOG_REPOSITORY',
+);
+ 
+export interface NotificationLogRepositoryPort {
+    findOneForReview(
+        reviewId: number,
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null>;
+ 
+    findOneForCycle(
+        cycleId: number,
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null>;
+ 
+    /**
+     * Find a standalone (no review/cycle) notification for a user by kind.
+     * Used for user-scoped notifications such as USER_WELCOME, where the
+     * dedup key is purely (recipientUserId, kind).
+     */
+    findOneForUser(
+        kind: NotificationKind,
+        recipientUserId: number,
+    ): Promise<NotificationLogDomain | null>;
+ 
+    create(log: NotificationLogDomain): Promise<NotificationLogDomain>;
+ 
+    markSent(id: number): Promise<void>;
+ 
+    markFailure(id: number, errorMessage: string): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/services/index.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/services/index.html new file mode 100644 index 00000000..b5f309a7 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/services/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for notifications/application/services + + + + + + + + + +
+
+

All files notifications/application/services

+
+ +
+ 95.16% + Statements + 118/124 +
+ + +
+ 70.83% + Branches + 68/96 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 95.08% + Lines + 116/122 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
review-email-notification.service.ts +
+
95.16%118/12470.83%68/96100%9/995.08%116/122
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/notifications/application/services/review-email-notification.service.ts.html b/apps/api/test/unit/coverage/lcov-report/notifications/application/services/review-email-notification.service.ts.html new file mode 100644 index 00000000..40fa4734 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/notifications/application/services/review-email-notification.service.ts.html @@ -0,0 +1,1264 @@ + + + + + + Code coverage report for notifications/application/services/review-email-notification.service.ts + + + + + + + + + +
+
+

All files / notifications/application/services review-email-notification.service.ts

+
+ +
+ 95.16% + Statements + 118/124 +
+ + +
+ 70.83% + Branches + 68/96 +
+ + +
+ 100% + Functions + 9/9 +
+ + +
+ 95.08% + Lines + 116/122 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +3944x +4x +4x +  +  +  +4x +  +  +  +4x +  +  +  +4x +4x +  +  +  +  +4x +  +  +  +4x +4x +4x +  +  +  +  +  +4x +4x +  +  +  +  +  +4x +19x +  +  +19x +  +19x +  +19x +  +19x +  +19x +  +19x +  +19x +19x +19x +  +  +  +  +  +  +  +  +  +  +  +8x +8x +1x +1x +  +  +7x +  +  +  +  +  +  +  +  +  +  +  +7x +  +  +  +  +  +  +  +  +2x +  +2x +  +2x +2x +1x +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +3x +3x +1x +1x +  +  +2x +2x +1x +  +  +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +2x +2x +1x +1x +  +  +1x +  +  +  +  +1x +  +1x +2x +2x +1x +  +  +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +  +  +  +3x +3x +1x +1x +  +  +2x +2x +1x +  +  +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +2x +2x +1x +1x +  +  +1x +  +1x +  +1x +1x +1x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +  +12x +  +  +  +12x +  +  +  +12x +1x +  +  +1x +  +  +11x +  +  +11x +4x +  +  +  +  +7x +1x +  +  +  +  +  +6x +  +  +  +  +  +11x +1x +  +  +1x +  +  +  +10x +  +  +  +  +  +  +  +  +  +  +10x +  +  +  +  +  +  +  +  +10x +10x +  +  +  +  +  +  +  +  +  +  +  +  +  +9x +9x +  +  +9x +  +  +1x +1x +  +  +  +1x +1x +  +  +  +  +12x +12x +12x +12x +  +  + 
import { Inject, Injectable, Logger } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/respondent.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/review.repository.port';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/reviewer.repository.port';
+import { CycleService } from 'src/contexts/feedback360/application/services/cycle.service';
+import {
+    IDENTITY_USER_REPOSITORY,
+    UserRepositoryPort,
+} from 'src/contexts/identity/application/ports/user.repository.port';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import {
+    STRATEGIC_REPORT_REPOSITORY,
+    StrategicReportRepositoryPort,
+} from 'src/contexts/reporting/application/ports/strategic-report.repository.port';
+import { NotificationKind } from '../../domain/notification-kind.enum';
+import { NotificationLogDomain } from '../../domain/notification-log.domain';
+import {
+    LOGO_BUFFER,
+    LOGO_CID,
+    LOGO_CONTENT_TYPE,
+    LOGO_FILENAME,
+} from '../../infrastructure/templates/logo-data-uri';
+import { MAILER, MailerPort } from '../ports/mailer.port';
+import {
+    NOTIFICATION_LOG_REPOSITORY,
+    NotificationLogRepositoryPort,
+} from '../ports/notification-log.repository.port';
+ 
+@Injectable()
+export class ReviewEmailNotificationService {
+    private readonly logger = new Logger(ReviewEmailNotificationService.name);
+ 
+    constructor(
+        @Inject(MAILER) private readonly mailer: MailerPort,
+        @Inject(NOTIFICATION_LOG_REPOSITORY)
+        private readonly notificationLogRepo: NotificationLogRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviewRepo: ReviewRepositoryPort,
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondentRepo: RespondentRepositoryPort,
+        @Inject(REVIEWER_REPOSITORY)
+        private readonly reviewerRepo: ReviewerRepositoryPort,
+        @Inject(IDENTITY_USER_REPOSITORY)
+        private readonly userRepo: UserRepositoryPort,
+        @Inject(STRATEGIC_REPORT_REPOSITORY)
+        private readonly strategicReportRepo: StrategicReportRepositoryPort,
+        private readonly cycleService: CycleService,
+        private readonly configService: ConfigService,
+    ) {}
+ 
+    /**
+     * Send a welcome email to a freshly registered user. Triggered from
+     * the identity context when a new user is created.
+     *
+     * Dedup is per-user (regardless of review/cycle), so re-creating the
+     * same user-id (which doesn't really happen) or replaying the event
+     * won't cause duplicate emails.
+     */
+    async notifyUserWelcome(userId: number): Promise<number> {
+        const user = await this.userRepo.findById(userId);
+        if (!user) {
+            this.logger.warn(`User ${userId} not found for welcome email`);
+            return 0;
+        }
+ 
+        const sent = await this.dispatch({
+            owner: { kind: 'user', id: userId },
+            kind: NotificationKind.USER_WELCOME,
+            recipient: user,
+            subject: 'Welcome to Intra · Feedback360',
+            template: 'user-welcome',
+            actionUrl: this.buildUrl('dashboard'),
+            extraContext: {
+                userFirstName: user.firstName,
+            },
+        });
+ 
+        return sent ? 1 : 0;
+    }
+ 
+    /**
+     * Notify the HR responsible for the cycle that the strategic
+     * (cycle-level) report is ready. Triggered when the cycle reaches
+     * `CycleStage.PUBLISHED`.
+     */
+    async notifyCycleStrategicReportReady(cycleId: number): Promise<number> {
+        const cycle = await this.cycleService.getById(cycleId);
+        const strategicReport =
+            await this.strategicReportRepo.findByCycleId(cycleId);
+ 
+        const hr = await this.userRepo.findById(cycle.hrId);
+        if (!hr) {
+            this.logger.warn(`HR ${cycle.hrId} not found for cycle ${cycleId}`);
+            return 0;
+        }
+ 
+        const sent = await this.dispatch({
+            owner: { kind: 'cycle', id: cycleId },
+            kind: NotificationKind.CYCLE_STRATEGIC_REPORT_READY,
+            recipient: hr,
+            subject: `Strategic report ready: ${cycle.title}`,
+            template: 'cycle-strategic-report-ready',
+            actionUrl: this.buildUrl(
+                `reporting/strategic-reports/${strategicReport?.id}`,
+            ),
+            extraContext: {
+                hrFirstName: hr.firstName,
+                cycleTitle: cycle.title,
+            },
+        });
+ 
+        return sent ? 1 : 0;
+    }
+ 
+    async notifyRateeSelfAssessment(reviewId: number): Promise<number> {
+        const review = await this.reviewRepo.findById(reviewId);
+        if (!review) {
+            this.logger.warn(`Review ${reviewId} not found`);
+            return 0;
+        }
+ 
+        const ratee = await this.userRepo.findById(review.rateeId);
+        if (!ratee) {
+            this.logger.warn(
+                `Ratee ${review.rateeId} not found for review ${reviewId}`,
+            );
+            return 0;
+        }
+ 
+        const sent = await this.dispatch({
+            owner: { kind: 'review', id: review.id! },
+            kind: NotificationKind.RATEE_SELF_ASSESSMENT,
+            recipient: ratee,
+            subject: 'A new 360° review has been created for you',
+            template: 'ratee-self-assessment',
+            actionUrl: this.buildUrl(`feedback360/surveys/${review.id}`),
+            extraContext: {
+                rateeFirstName: ratee.firstName,
+            },
+        });
+ 
+        return sent ? 1 : 0;
+    }
+ 
+    async notifyRespondents(reviewId: number): Promise<number> {
+        const review = await this.reviewRepo.findById(reviewId);
+        if (!review) {
+            this.logger.warn(`Review ${reviewId} not found`);
+            return 0;
+        }
+ 
+        const respondents = await this.respondentRepo.listByReview(
+            reviewId,
+            {},
+        );
+ 
+        let sentCount = 0;
+ 
+        for (const respondent of respondents) {
+            const user = await this.userRepo.findById(respondent.respondentId);
+            if (!user) {
+                this.logger.warn(
+                    `Respondent user ${respondent.respondentId} not found`,
+                );
+                continue;
+            }
+ 
+            const sent = await this.dispatch({
+                owner: { kind: 'review', id: review.id! },
+                kind: NotificationKind.RESPONDENT_INVITATION,
+                recipient: user,
+                subject: `Your feedback is requested for ${review.rateeFullName}`,
+                template: 'respondent-invitation',
+                actionUrl: this.buildUrl(`feedback360/surveys/${review.id}`),
+                extraContext: {
+                    respondentFirstName: user.firstName,
+                    rateeFullName: review.rateeFullName,
+                    rateeFirstName: review.rateeFullName.split(' ')[0],
+                },
+            });
+ 
+            Eif (sent) {
+                sentCount += 1;
+            }
+        }
+ 
+        return sentCount;
+    }
+ 
+    async notifyHrReportReady(reviewId: number): Promise<number> {
+        const review = await this.reviewRepo.findById(reviewId);
+        if (!review) {
+            this.logger.warn(`Review ${reviewId} not found`);
+            return 0;
+        }
+ 
+        const hr = await this.userRepo.findById(review.hrId);
+        if (!hr) {
+            this.logger.warn(
+                `HR ${review.hrId} not found for review ${reviewId}`,
+            );
+            return 0;
+        }
+ 
+        const sent = await this.dispatch({
+            owner: { kind: 'review', id: review.id! },
+            kind: NotificationKind.HR_REPORT_READY,
+            recipient: hr,
+            subject: `Review report ready: ${review.rateeFullName}`,
+            template: 'hr-report-ready',
+            actionUrl: this.buildUrl(
+                `reporting/individual-reports/${review.reportId}/comments`,
+            ),
+            extraContext: {
+                hrFirstName: hr.firstName,
+                rateeFullName: review.rateeFullName,
+            },
+        });
+ 
+        return sent ? 1 : 0;
+    }
+ 
+    async notifyReviewers(reviewId: number): Promise<number> {
+        const review = await this.reviewRepo.findById(reviewId);
+        if (!review) {
+            this.logger.warn(`Review ${reviewId} not found`);
+            return 0;
+        }
+ 
+        const reviewers = await this.reviewerRepo.listByReview(reviewId, {});
+ 
+        let sentCount = 0;
+ 
+        for (const reviewer of reviewers) {
+            const user = await this.userRepo.findById(reviewer.reviewerId);
+            Iif (!user) {
+                this.logger.warn(
+                    `Reviewer user ${reviewer.reviewerId} not found`,
+                );
+                continue;
+            }
+ 
+            const sent = await this.dispatch({
+                owner: { kind: 'review', id: review.id! },
+                kind: NotificationKind.REVIEWER_REPORT_READY,
+                recipient: user,
+                subject: `Individual report ready: ${review.rateeFullName}`,
+                template: 'reviewer-report-ready',
+                actionUrl: this.buildUrl(
+                    `reporting/individual-reports/${review.reportId}`,
+                ),
+                extraContext: {
+                    reviewerFirstName: user.firstName,
+                    rateeFullName: review.rateeFullName,
+                },
+            });
+ 
+            Eif (sent) {
+                sentCount += 1;
+            }
+        }
+ 
+        return sentCount;
+    }
+ 
+    private async dispatch(args: {
+        owner:
+            | { kind: 'review'; id: number }
+            | { kind: 'cycle'; id: number }
+            | { kind: 'user'; id: number };
+        kind: NotificationKind;
+        recipient: UserDomain;
+        subject: string;
+        template: string;
+        actionUrl: string;
+        extraContext: Record<string, unknown>;
+    }): Promise<boolean> {
+        const { owner, kind, recipient, subject, template, actionUrl } = args;
+ 
+        Iif (!owner.id) {
+            this.logger.warn(`${owner.kind} without id, skipping ${kind}`);
+            return false;
+        }
+        Iif (!recipient.id) {
+            this.logger.warn(`Recipient without id, skipping ${kind}`);
+            return false;
+        }
+        if (!recipient.email) {
+            this.logger.warn(
+                `Recipient ${recipient.id} has no email, skipping ${kind}`,
+            );
+            return false;
+        }
+ 
+        const ownerLabel = `${owner.kind}=${owner.id}`;
+ 
+        let existing;
+        if (owner.kind === 'review') {
+            existing = await this.notificationLogRepo.findOneForReview(
+                owner.id,
+                kind,
+                recipient.id,
+            );
+        } else if (owner.kind === 'cycle') {
+            existing = await this.notificationLogRepo.findOneForCycle(
+                owner.id,
+                kind,
+                recipient.id,
+            );
+        } else {
+            existing = await this.notificationLogRepo.findOneForUser(
+                kind,
+                recipient.id,
+            );
+        }
+ 
+        if (existing?.isSent()) {
+            this.logger.debug(
+                `Skipping ${kind} for ${ownerLabel} user=${recipient.id} (already sent)`,
+            );
+            return false;
+        }
+ 
+        const log =
+            existing ??
+            (await this.notificationLogRepo.create(
+                NotificationLogDomain.create({
+                    reviewId: owner.kind === 'review' ? owner.id : null,
+                    cycleId: owner.kind === 'cycle' ? owner.id : null,
+                    kind,
+                    recipientUserId: recipient.id,
+                    recipientEmail: recipient.email,
+                }),
+            ));
+ 
+        const context = {
+            ...args.extraContext,
+            actionUrl,
+            subject,
+            supportEmail: this.configService.get<string>('app.supportEmail'),
+            logoCid: LOGO_CID,
+            appName: 'Intra',
+        };
+ 
+        try {
+            await this.mailer.sendMail({
+                to: recipient.email,
+                subject,
+                template,
+                context,
+                attachments: [
+                    {
+                        filename: LOGO_FILENAME,
+                        content: LOGO_BUFFER,
+                        contentType: LOGO_CONTENT_TYPE,
+                        cid: LOGO_CID,
+                    },
+                ],
+            });
+            await this.notificationLogRepo.markSent(log.id!);
+            this.logger.log(
+                `Sent ${kind} for ${ownerLabel} to user=${recipient.id}`,
+            );
+            return true;
+        } catch (error) {
+            const message =
+                error instanceof Error ? error.message : String(error);
+            this.logger.error(
+                `Failed to send ${kind} for ${ownerLabel} to user=${recipient.id}: ${message}`,
+                error instanceof Error ? error.stack : undefined,
+            );
+            await this.notificationLogRepo.markFailure(log.id!, message);
+            return false;
+        }
+    }
+ 
+    private buildUrl(path: string): string {
+        const base = this.configService.get<string>('app.frontendLink') ?? '/';
+        const trimmedBase = base.endsWith('/') ? base.slice(0, -1) : base;
+        const trimmedPath = path.startsWith('/') ? path.slice(1) : path;
+        return `${trimmedBase}/${trimmedPath}`;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/index.html b/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/index.html new file mode 100644 index 00000000..e326b29d --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for organisation/application/ports + + + + + + + + + +
+
+

All files organisation/application/ports

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
position-hierarchy.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
position.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
team.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/position-hierarchy.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/position-hierarchy.repository.port.ts.html new file mode 100644 index 00000000..f7eaefef --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/position-hierarchy.repository.port.ts.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for organisation/application/ports/position-hierarchy.repository.port.ts + + + + + + + + + +
+
+

All files / organisation/application/ports position-hierarchy.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { PositionHierarchyDomain } from '../../domain/position-hierarchy.domain';
+ 
+export const ORGANISATION_POSITION_HIERARCHY_REPOSITORY = Symbol(
+    'ORGANISATION.POSITION_HIERARCHY_REPOSITORY',
+);
+ 
+export interface PositionHierarchyRepositoryPort {
+    link(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<PositionHierarchyDomain>;
+    unlink(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<void>;
+    listSubordinates(
+        superiorPositionId: number,
+    ): Promise<PositionHierarchyDomain[]>;
+    listSuperiors(
+        subordinatePositionId: number,
+    ): Promise<PositionHierarchyDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/position.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/position.repository.port.ts.html new file mode 100644 index 00000000..236520cf --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/position.repository.port.ts.html @@ -0,0 +1,145 @@ + + + + + + Code coverage report for organisation/application/ports/position.repository.port.ts + + + + + + + + + +
+
+

All files / organisation/application/ports position.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21  +  +  +  +  +  +24x +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+    PositionSearchQuery,
+    UpdatePositionPayload,
+} from '@intra/shared-kernel';
+import { PositionDomain } from '../../domain/position.domain';
+ 
+export const ORGANISATION_POSITION_REPOSITORY = Symbol(
+    'ORGANISATION.POSITION_REPOSITORY',
+);
+ 
+export interface PositionRepositoryPort {
+    create(position: PositionDomain): Promise<PositionDomain>;
+    findById(id: number): Promise<PositionDomain | null>;
+    search(query: PositionSearchQuery): Promise<PositionDomain[]>;
+    updateById(
+        id: number,
+        patch: UpdatePositionPayload,
+    ): Promise<PositionDomain>;
+    deleteById(id: number): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/team.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/team.repository.port.ts.html new file mode 100644 index 00000000..8125e540 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/organisation/application/ports/team.repository.port.ts.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for organisation/application/ports/team.repository.port.ts + + + + + + + + + +
+
+

All files / organisation/application/ports team.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23  +  +  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { TeamSearchQuery, UpdateTeamPayload } from '@intra/shared-kernel';
+import { TeamMembershipDomain } from '../../domain/team-membership.domain';
+import { TeamDomain } from '../../domain/team.domain';
+ 
+export const ORGANISATION_TEAM_REPOSITORY = Symbol(
+    'ORGANISATION.TEAM_REPOSITORY',
+);
+ 
+export interface TeamRepositoryPort {
+    create(team: TeamDomain): Promise<TeamDomain>;
+    findById(id: number): Promise<TeamDomain | null>;
+    search(query: TeamSearchQuery): Promise<TeamDomain[]>;
+    updateById(id: number, patch: UpdateTeamPayload): Promise<TeamDomain>;
+    deleteById(id: number): Promise<void>;
+    addMember(
+        teamId: number,
+        memberId: number,
+        isPrimary?: boolean | null,
+    ): Promise<TeamMembershipDomain>;
+    removeMember(teamId: number, memberId: number): Promise<void>;
+    listMembers(teamId: number): Promise<TeamMembershipDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/organisation/application/services/index.html b/apps/api/test/unit/coverage/lcov-report/organisation/application/services/index.html new file mode 100644 index 00000000..48e69621 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/organisation/application/services/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for organisation/application/services + + + + + + + + + +
+
+

All files organisation/application/services

+
+ +
+ 100% + Statements + 98/98 +
+ + +
+ 88.63% + Branches + 39/44 +
+ + +
+ 100% + Functions + 25/25 +
+ + +
+ 100% + Lines + 88/88 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
position-hierarchy.service.ts +
+
100%24/2483.33%5/6100%7/7100%22/22
position.service.ts +
+
100%28/2875%9/12100%8/8100%24/24
team.service.ts +
+
100%46/4696.15%25/26100%10/10100%42/42
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/organisation/application/services/position-hierarchy.service.ts.html b/apps/api/test/unit/coverage/lcov-report/organisation/application/services/position-hierarchy.service.ts.html new file mode 100644 index 00000000..4dcd4db4 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/organisation/application/services/position-hierarchy.service.ts.html @@ -0,0 +1,286 @@ + + + + + + Code coverage report for organisation/application/services/position-hierarchy.service.ts + + + + + + + + + +
+
+

All files / organisation/application/services position-hierarchy.service.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 83.33% + Branches + 5/6 +
+ + +
+ 100% + Functions + 7/7 +
+ + +
+ 100% + Lines + 22/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +682x +  +  +  +2x +2x +  +  +2x +  +  +5x +5x +  +  +  +  +  +  +2x +1x +  +  +  +  +1x +1x +  +1x +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +1x +  +1x +1x +  +2x +  +  +  +  +  +  +  +1x +1x +  +  +1x +  +2x +  +  +  +  + 
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
+import { PositionHierarchyDomain } from '../../domain/position-hierarchy.domain';
+import { PositionDomain } from '../../domain/position.domain';
+import type { PositionHierarchyRepositoryPort } from '../ports/position-hierarchy.repository.port';
+import { ORGANISATION_POSITION_HIERARCHY_REPOSITORY } from '../ports/position-hierarchy.repository.port';
+import { PositionService } from './position.service';
+ 
+@Injectable()
+export class PositionHierarchyService {
+    constructor(
+        @Inject(ORGANISATION_POSITION_HIERARCHY_REPOSITORY)
+        private readonly hierarchy: PositionHierarchyRepositoryPort,
+        private readonly positions: PositionService,
+    ) {}
+ 
+    async link(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<PositionHierarchyDomain> {
+        if (superiorPositionId === subordinatePositionId) {
+            throw new BadRequestException(
+                'Position cannot be a subordinate of itself',
+            );
+        }
+ 
+        await this.positions.getById(superiorPositionId);
+        await this.positions.getById(subordinatePositionId);
+ 
+        return this.hierarchy.link(superiorPositionId, subordinatePositionId);
+    }
+ 
+    async unlink(
+        superiorPositionId: number,
+        subordinatePositionId: number,
+    ): Promise<void> {
+        await this.positions.getById(superiorPositionId);
+        await this.positions.getById(subordinatePositionId);
+        await this.hierarchy.unlink(superiorPositionId, subordinatePositionId);
+    }
+ 
+    async listSubordinates(
+        superiorPositionId: number,
+    ): Promise<PositionDomain[]> {
+        await this.positions.getById(superiorPositionId);
+        const relations =
+            await this.hierarchy.listSubordinates(superiorPositionId);
+        return Promise.all(
+            relations.map((rel) =>
+                this.positions.getById(rel.subordinatePositionId),
+            ),
+        );
+    }
+ 
+    async listSuperiors(
+        subordinatePositionId: number,
+    ): Promise<PositionDomain[]> {
+        await this.positions.getById(subordinatePositionId);
+        const relations = await this.hierarchy.listSuperiors(
+            subordinatePositionId,
+        );
+        return Promise.all(
+            relations.map((rel) =>
+                this.positions.getById(rel.superiorPositionId),
+            ),
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/organisation/application/services/position.service.ts.html b/apps/api/test/unit/coverage/lcov-report/organisation/application/services/position.service.ts.html new file mode 100644 index 00000000..5ed2708c --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/organisation/application/services/position.service.ts.html @@ -0,0 +1,337 @@ + + + + + + Code coverage report for organisation/application/services/position.service.ts + + + + + + + + + +
+
+

All files / organisation/application/services position.service.ts

+
+ +
+ 100% + Statements + 28/28 +
+ + +
+ 75% + Branches + 9/12 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 24/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85  +  +  +  +  +19x +  +  +  +  +  +19x +  +  +  +19x +  +19x +  +  +19x +  +  +11x +  +11x +  +  +  +2x +  +  +  +2x +  +  +  +1x +  +  +  +10x +10x +7x +  +  +  +  +  +  +2x +1x +  +  +  +  +  +1x +  +  +  +3x +2x +2x +1x +  +  +  +  +  +  +  +  +  +1x +  +  +  +3x +  +3x +3x +  +  + 
import {
+    CreatePositionPayload,
+    PositionSearchQuery,
+    UpdatePositionPayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import {
+    POSITION_COMPETENCE_RELATION_REPOSITORY,
+    PositionCompetenceRelationRepositoryPort,
+} from 'src/contexts/library/application/ports/position-competence-relation.repository.port';
+import { PositionDomain } from '../../domain/position.domain';
+import type { PositionRepositoryPort } from '../ports/position.repository.port';
+import { ORGANISATION_POSITION_REPOSITORY } from '../ports/position.repository.port';
+ 
+@Injectable()
+export class PositionService {
+    constructor(
+        @Inject(ORGANISATION_POSITION_REPOSITORY)
+        private readonly positions: PositionRepositoryPort,
+        @Inject(POSITION_COMPETENCE_RELATION_REPOSITORY)
+        private readonly positionCompetenceRelations: PositionCompetenceRelationRepositoryPort,
+    ) {}
+ 
+    async create(payload: CreatePositionPayload): Promise<PositionDomain> {
+        const position = PositionDomain.create({
+            title: payload.title,
+            description: payload.description ?? null,
+        });
+        return this.positions.create(position);
+    }
+ 
+    async search(query: PositionSearchQuery): Promise<PositionDomain[]> {
+        return this.positions.search(query);
+    }
+ 
+    async getById(id: number): Promise<PositionDomain> {
+        const found = await this.positions.findById(id);
+        if (!found) throw new NotFoundException('Position not found');
+        return found;
+    }
+ 
+    async update(
+        id: number,
+        patch: UpdatePositionPayload,
+    ): Promise<PositionDomain> {
+        await this.getById(id);
+        const payload: UpdatePositionPayload = {
+            ...patch,
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+        };
+        return this.positions.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        const competences = await this.listCompetences(id);
+        if (competences.length > 0) {
+            throw new BadRequestException(
+                'Position #' +
+                    id +
+                    ' cannot be deleted. It has ' +
+                    competences.length +
+                    ' competences assigned to it.',
+            );
+        }
+        // await this.positionCompetenceRelations.deleteAllForPosition(id);
+        // await this.positionQuestionTemplateRelations.deleteAllForPosition(id);
+        await this.positions.deleteById(id);
+    }
+ 
+    async listCompetences(positionId: number): Promise<number[]> {
+        await this.getById(positionId);
+        const relations =
+            await this.positionCompetenceRelations.listByPosition(positionId);
+        return relations.map((r) => r.competenceId);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/organisation/application/services/team.service.ts.html b/apps/api/test/unit/coverage/lcov-report/organisation/application/services/team.service.ts.html new file mode 100644 index 00000000..1520e84d --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/organisation/application/services/team.service.ts.html @@ -0,0 +1,475 @@ + + + + + + Code coverage report for organisation/application/services/team.service.ts + + + + + + + + + +
+
+

All files / organisation/application/services team.service.ts

+
+ +
+ 100% + Statements + 46/46 +
+ + +
+ 96.15% + Branches + 25/26 +
+ + +
+ 100% + Functions + 10/10 +
+ + +
+ 100% + Lines + 42/42 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131  +  +  +  +  +  +2x +  +  +  +  +  +2x +  +2x +  +2x +  +  +2x +  +  +19x +19x +  +  +  +3x +1x +  +  +3x +  +  +  +  +3x +  +  +  +1x +  +  +  +17x +17x +14x +  +  +  +4x +  +3x +1x +  +  +3x +  +  +  +  +  +  +3x +  +  +  +3x +2x +2x +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +2x +2x +  +2x +  +  +  +  +2x +1x +  +1x +  +  +  +1x +1x +  +  +  +  +  +  +5x +5x +  +5x +  +2x +  +3x +3x +  +  +2x +  +1x +  +  +  +  +2x +  +  + 
import {
+    AddTeamMemberPayload,
+    CreateTeamPayload,
+    TeamSearchQuery,
+    UpdateTeamPayload,
+} from '@intra/shared-kernel';
+import {
+    BadRequestException,
+    Inject,
+    Injectable,
+    NotFoundException,
+} from '@nestjs/common';
+import { IdentityUserService } from 'src/contexts/identity/application/services/identity-user.service';
+import { TeamMembershipDomain } from '../../domain/team-membership.domain';
+import { TeamDomain } from '../../domain/team.domain';
+import type { TeamRepositoryPort } from '../ports/team.repository.port';
+import { ORGANISATION_TEAM_REPOSITORY } from '../ports/team.repository.port';
+ 
+@Injectable()
+export class TeamService {
+    constructor(
+        @Inject(ORGANISATION_TEAM_REPOSITORY)
+        private readonly teams: TeamRepositoryPort,
+        private readonly identityUsers: IdentityUserService,
+    ) {}
+ 
+    async create(payload: CreateTeamPayload): Promise<TeamDomain> {
+        if (payload.headId !== undefined && payload.headId !== null) {
+            await this.identityUsers.getById(payload.headId);
+        }
+ 
+        const team = TeamDomain.create({
+            title: payload.title,
+            description: payload.description ?? null,
+            headId: payload.headId ?? null,
+        });
+        return this.teams.create(team);
+    }
+ 
+    async search(query: TeamSearchQuery): Promise<TeamDomain[]> {
+        return this.teams.search(query);
+    }
+ 
+    async getById(id: number): Promise<TeamDomain> {
+        const team = await this.teams.findById(id);
+        if (!team) throw new NotFoundException('Team not found');
+        return team;
+    }
+ 
+    async update(id: number, patch: UpdateTeamPayload): Promise<TeamDomain> {
+        await this.getById(id);
+ 
+        if (patch.headId !== undefined && patch.headId !== null) {
+            await this.identityUsers.getById(patch.headId);
+        }
+ 
+        const payload: UpdateTeamPayload = {
+            ...patch,
+            ...(patch.description !== undefined
+                ? { description: patch.description }
+                : {}),
+        };
+ 
+        return this.teams.updateById(id, payload);
+    }
+ 
+    async delete(id: number): Promise<void> {
+        await this.getById(id);
+        const members = await this.listMembers(id);
+        if (members.length > 0) {
+            throw new BadRequestException(
+                'Team #' +
+                    id +
+                    ' cannot be deleted. It has ' +
+                    members.length +
+                    ' members assigned to it.',
+            );
+        }
+        await this.teams.deleteById(id);
+    }
+ 
+    async addMember(
+        teamId: number,
+        payload: AddTeamMemberPayload,
+        opts?: { withUser?: boolean },
+    ): Promise<TeamMembershipDomain> {
+        await this.getById(teamId);
+        const user = await this.identityUsers.getById(payload.memberId);
+ 
+        const membership = await this.teams.addMember(
+            teamId,
+            payload.memberId,
+            payload.isPrimary,
+        );
+        if (opts?.withUser) {
+            return membership.withUser(user);
+        }
+        return membership;
+    }
+ 
+    async removeMember(teamId: number, memberId: number): Promise<void> {
+        await this.getById(teamId);
+        await this.teams.removeMember(teamId, memberId);
+    }
+ 
+    async listMembers(
+        teamId: number,
+        opts?: { withUsers?: boolean },
+    ): Promise<TeamMembershipDomain[]> {
+        await this.getById(teamId);
+        const memberships = await this.teams.listMembers(teamId);
+ 
+        if (!opts?.withUsers) return memberships;
+ 
+        const withUsers = await Promise.all(
+            memberships.map(async (membership) => {
+                try {
+                    const user = await this.identityUsers.getById(
+                        membership.memberId,
+                    );
+                    return membership.withUser(user);
+                } catch {
+                    return membership;
+                }
+            }),
+        );
+ 
+        return withUsers;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/prettify.css b/apps/api/test/unit/coverage/lcov-report/prettify.css new file mode 100644 index 00000000..b317a7cd --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/apps/api/test/unit/coverage/lcov-report/prettify.js b/apps/api/test/unit/coverage/lcov-report/prettify.js new file mode 100644 index 00000000..b3225238 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/cycle-stage.listener.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/cycle-stage.listener.ts.html new file mode 100644 index 00000000..46189b11 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/cycle-stage.listener.ts.html @@ -0,0 +1,343 @@ + + + + + + Code coverage report for reporting/application/listeners/cycle-stage.listener.ts + + + + + + + + + +
+
+

All files / reporting/application/listeners cycle-stage.listener.ts

+
+ +
+ 89.65% + Statements + 26/29 +
+ + +
+ 66.66% + Branches + 16/24 +
+ + +
+ 66.66% + Functions + 2/3 +
+ + +
+ 88.88% + Lines + 24/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +871x +1x +1x +1x +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +1x +3x +  +  +3x +3x +  +3x +  +  +  +  +  +  +  +  +  +  +1x +  +  +3x +1x +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +2x +  +  +  +2x +2x +  +  +  +  +  +2x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  + 
import { CycleStage } from '@intra/shared-kernel';
+import { Inject, Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { StrategicReportingService } from 'src/contexts/reporting/application/services/strategic-reports.service';
+import { CycleStageProcessedEvent } from '../../../feedback360/application/events/cycle-stage-processed.event';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../../../feedback360/application/ports/review.repository.port';
+import { CycleService } from '../../../feedback360/application/services/cycle.service';
+ 
+/**
+ * Event listener for cycle stage changes
+ * Automatically triggers strategic report generation
+ * when cycle reaches FINISHED stage
+ */
+@Injectable()
+export class CycleStageListener {
+    private readonly logger = new Logger(CycleStageListener.name);
+ 
+    constructor(
+        private readonly strategicReportingService: StrategicReportingService,
+        private readonly cycleService: CycleService,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+    ) {}
+ 
+    /*
+     * Handles the cycle.stage.processed event
+     * Triggers strategic report generation for the cycle
+     * when it transitions to the FINISHED stage
+     * updates review stage to PREPARING_REPORT
+     * and then moves it to PUBLISHED stage
+     */
+    @OnEvent('cycle.stage.processed')
+    async handleCycleStageProcessed(
+        event: CycleStageProcessedEvent,
+    ): Promise<void> {
+        if (event.currentStage !== CycleStage.FINISHED) {
+            return;
+        }
+ 
+        this.logger.debug(
+            `Cycle ${event.cycleId} with stage ${event.currentStage} was processed`,
+        );
+ 
+        setTimeout(async () => {
+            const reviews = await this.reviews.listByCycleId(event.cycleId);
+            if (reviews.length === 0) {
+                this.logger.debug(
+                    `No reviews found for cycle ${event.cycleId}. Skipping strategic report generation.`,
+                );
+            }
+        }, 10000);
+ 
+        this.logger.debug(
+            `Initiating strategic report generation for cycle ${event.cycleId}`,
+        );
+ 
+        try {
+            await this.cycleService.changeStage(
+                event.cycleId,
+                CycleStage.PREPARING_REPORT,
+            );
+ 
+            const report =
+                await this.strategicReportingService.generateStrategicReportForCycle(
+                    event.cycleId,
+                );
+ 
+            this.logger.debug(
+                `Successfully generated strategic report ${report.id} for cycle ${event.cycleId}`,
+            );
+ 
+            await this.cycleService.changeStage(
+                event.cycleId,
+                CycleStage.PUBLISHED,
+            );
+        } catch (error) {
+            this.logger.error(
+                `Failed to generate strategic report for cycle ${event.cycleId}: ${error.message}`,
+                error.stack,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/index.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/index.html new file mode 100644 index 00000000..2b88ef7c --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for reporting/application/listeners + + + + + + + + + +
+
+

All files reporting/application/listeners

+
+ +
+ 94.23% + Statements + 49/52 +
+ + +
+ 71.42% + Branches + 30/42 +
+ + +
+ 80% + Functions + 4/5 +
+ + +
+ 93.75% + Lines + 45/48 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
cycle-stage.listener.ts +
+
89.65%26/2966.66%16/2466.66%2/388.88%24/27
review-stage.listener.ts +
+
100%23/2377.77%14/18100%2/2100%21/21
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/review-stage.listener.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/review-stage.listener.ts.html new file mode 100644 index 00000000..68d1b5f4 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/listeners/review-stage.listener.ts.html @@ -0,0 +1,292 @@ + + + + + + Code coverage report for reporting/application/listeners/review-stage.listener.ts + + + + + + + + + +
+
+

All files / reporting/application/listeners review-stage.listener.ts

+
+ +
+ 100% + Statements + 23/23 +
+ + +
+ 77.77% + Branches + 14/18 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 21/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +701x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +1x +3x +  +  +3x +3x +  +  +  +  +  +  +  +  +  +  +1x +  +  +3x +1x +  +  +2x +  +  +2x +  +  +  +2x +2x +  +  +  +  +2x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  + 
import { ReviewStage } from '@intra/shared-kernel';
+import { Injectable, Logger } from '@nestjs/common';
+import { OnEvent } from '@nestjs/event-emitter';
+import { ReviewStageProcessedEvent } from 'src/contexts/feedback360/application/events/review-stage-processed.event';
+import { ReviewService } from '../../../feedback360/application/services/review.service';
+import { ReportingService } from '../services/reports.service';
+ 
+/**
+ * Event listener for review stage changes
+ * Automatically triggers individual report generation
+ * when review reaches FINISHED stage
+ */
+@Injectable()
+export class ReviewStageListener {
+    private readonly logger = new Logger(ReviewStageListener.name);
+ 
+    constructor(
+        private readonly reportService: ReportingService,
+        private readonly reviewService: ReviewService,
+    ) {}
+ 
+    /**
+     * Handles review.stage.processed events
+     * Triggers individual report generation for the review
+     * when review transitions to FINISHED stage
+     * updates review stage to PREPARING_REPORT
+     * and then moves it to PROCESSING_BY_HR stage
+     */
+    @OnEvent('review.stage.processed')
+    async handleReviewStageProcessed(
+        event: ReviewStageProcessedEvent,
+    ): Promise<void> {
+        if (event.currentStage !== ReviewStage.FINISHED) {
+            return;
+        }
+ 
+        this.logger.debug(
+            `Review ${event.reviewId} with stage ${event.currentStage} was processed`,
+        );
+        this.logger.debug(
+            `Initiating individual report generation for review ${event.reviewId}`,
+        );
+ 
+        try {
+            await this.reviewService.changeReviewStage(
+                event.reviewId,
+                ReviewStage.PREPARING_REPORT,
+            );
+ 
+            const report = await this.reportService.generateReportForReview(
+                event.reviewId,
+            );
+ 
+            this.logger.debug(
+                `Successfully generated individual report ${report.id} for review ${event.reviewId}`,
+            );
+ 
+            await this.reviewService.changeReviewStage(
+                event.reviewId,
+                ReviewStage.PROCESSING_BY_HR,
+            );
+        } catch (error) {
+            this.logger.error(
+                `Failed to generate individual report for review ${event.reviewId}: ${error.message}`,
+                error.stack,
+            );
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/index.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/index.html new file mode 100644 index 00000000..4e589b91 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/index.html @@ -0,0 +1,206 @@ + + + + + + Code coverage report for reporting/application/ports + + + + + + + + + +
+
+

All files reporting/application/ports

+
+ +
+ 100% + Statements + 7/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 7/7 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
report-analytics.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
report-comment.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
report-insight.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
report.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
strategic-report-analytics.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
strategic-report-insight.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
strategic-report.repository.port.ts +
+
100%1/1100%0/0100%0/0100%1/1
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-analytics.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-analytics.repository.port.ts.html new file mode 100644 index 00000000..87f9874a --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-analytics.repository.port.ts.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for reporting/application/ports/report-analytics.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports report-analytics.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +  +11x +  +  +  +  +  +  +  +  +  +  +  + 
import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+ 
+export const REPORT_ANALYTICS_REPOSITORY = Symbol(
+    'REPORTING.REPORT_ANALYTICS_REPOSITORY',
+);
+ 
+export interface ReportAnalyticsRepositoryPort {
+    findByReportId(reportId: number): Promise<ReportAnalyticsDomain[]>;
+    findById(id: number): Promise<ReportAnalyticsDomain | null>;
+    createMany(
+        reportId: number,
+        analytics: ReportAnalyticsDomain[],
+    ): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-comment.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-comment.repository.port.ts.html new file mode 100644 index 00000000..0862a773 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-comment.repository.port.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for reporting/application/ports/report-comment.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports report-comment.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +6x +  +  +  +  +  +  +  +  + 
import { ReportCommentDomain } from '../../domain/report-comment.domain';
+ 
+export const REPORT_COMMENT_REPOSITORY = Symbol(
+    'REPORTING.REPORT_COMMENT_REPOSITORY',
+);
+ 
+export interface ReportCommentRepositoryPort {
+    findByReportId(reportId: number): Promise<ReportCommentDomain[]>;
+    findById(id: number): Promise<ReportCommentDomain | null>;
+    create(comment: ReportCommentDomain): Promise<ReportCommentDomain>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-insight.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-insight.repository.port.ts.html new file mode 100644 index 00000000..0f86f55a --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report-insight.repository.port.ts.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for reporting/application/ports/report-insight.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports report-insight.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +  +6x +  +  +  +  +  +  +  +  +  +  +  + 
import { ReportInsightDomain } from '../../domain/report-insight.domain';
+ 
+export const REPORT_INSIGHT_REPOSITORY = Symbol(
+    'REPORTING.REPORT_INSIGHT_REPOSITORY',
+);
+ 
+export interface ReportInsightRepositoryPort {
+    findByReportId(reportId: number): Promise<ReportInsightDomain[]>;
+    findById(id: number): Promise<ReportInsightDomain | null>;
+    createMany(
+        reportId: number,
+        insights: ReportInsightDomain[],
+    ): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report.repository.port.ts.html new file mode 100644 index 00000000..79c65748 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/report.repository.port.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for reporting/application/ports/report.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports report.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +11x +  +  +  +  +  +  +  + 
import { ReportSearchQuery } from '@intra/shared-kernel';
+import { ReportDomain } from '../../domain/report.domain';
+ 
+export const REPORT_REPOSITORY = Symbol('REPORTING.REPORT_REPOSITORY');
+ 
+export interface ReportRepositoryPort {
+    create(report: ReportDomain): Promise<ReportDomain>;
+    findById(id: number): Promise<ReportDomain | null>;
+    findByReviewId(reviewId: number): Promise<ReportDomain | null>;
+    search(query: ReportSearchQuery): Promise<ReportDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report-analytics.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report-analytics.repository.port.ts.html new file mode 100644 index 00000000..c103cb99 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report-analytics.repository.port.ts.html @@ -0,0 +1,133 @@ + + + + + + Code coverage report for reporting/application/ports/strategic-report-analytics.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports strategic-report-analytics.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { StrategicReportAnalyticsDomain } from '../../domain/strategic-report-analytics.domain';
+ 
+export const STRATEGIC_REPORT_ANALYTICS_REPOSITORY = Symbol(
+    'REPORTING.STRATEGIC_REPORT_ANALYTICS_REPOSITORY',
+);
+ 
+export interface StrategicReportAnalyticsRepositoryPort {
+    findByStrategicReportId(
+        strategicReportId: number,
+    ): Promise<StrategicReportAnalyticsDomain[]>;
+    findById(id: number): Promise<StrategicReportAnalyticsDomain | null>;
+    createMany(
+        strategicReportId: number,
+        analytics: StrategicReportAnalyticsDomain[],
+    ): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report-insight.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report-insight.repository.port.ts.html new file mode 100644 index 00000000..deeae106 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report-insight.repository.port.ts.html @@ -0,0 +1,133 @@ + + + + + + Code coverage report for reporting/application/ports/strategic-report-insight.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports strategic-report-insight.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17  +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { StrategicReportInsightDomain } from '../../domain/startegic-report-insight.domain';
+ 
+export const STRATEGIC_REPORT_INSIGHT_REPOSITORY = Symbol(
+    'REPORTING.STRATEGIC_REPORT_INSIGHT_REPOSITORY',
+);
+ 
+export interface StrategicReportInsightRepositoryPort {
+    findByStrategicReportId(
+        strategicReportId: number,
+    ): Promise<StrategicReportInsightDomain[]>;
+    findById(id: number): Promise<StrategicReportInsightDomain | null>;
+    createMany(
+        strategicReportId: number,
+        insights: StrategicReportInsightDomain[],
+    ): Promise<void>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report.repository.port.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report.repository.port.ts.html new file mode 100644 index 00000000..be8082f0 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/ports/strategic-report.repository.port.ts.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for reporting/application/ports/strategic-report.repository.port.ts + + + + + + + + + +
+
+

All files / reporting/application/ports strategic-report.repository.port.ts

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14  +  +  +22x +  +  +  +  +  +  +  +  +  + 
import { StrategicReportSearchQuery } from '@intra/shared-kernel';
+import { StrategicReportDomain } from '../../domain/strategic-report.domain';
+ 
+export const STRATEGIC_REPORT_REPOSITORY = Symbol(
+    'REPORTING.STRATEGIC_REPORT_REPOSITORY',
+);
+ 
+export interface StrategicReportRepositoryPort {
+    create(report: StrategicReportDomain): Promise<StrategicReportDomain>;
+    findById(id: number): Promise<StrategicReportDomain | null>;
+    findByCycleId(cycleId: number): Promise<StrategicReportDomain | null>;
+    search(query: StrategicReportSearchQuery): Promise<StrategicReportDomain[]>;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/index.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/index.html new file mode 100644 index 00000000..c6863a5c --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/index.html @@ -0,0 +1,221 @@ + + + + + + Code coverage report for reporting/application/services + + + + + + + + + +
+
+

All files reporting/application/services

+
+ +
+ 94.42% + Statements + 694/735 +
+ + +
+ 69.38% + Branches + 451/650 +
+ + +
+ 91.48% + Functions + 129/141 +
+ + +
+ 95.23% + Lines + 660/693 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
report-analytics.service.ts +
+
100%18/1875%9/12100%3/3100%16/16
report-comment.service.ts +
+
100%18/1878.57%11/14100%4/4100%16/16
report-insight.service.ts +
+
100%18/1875%9/12100%3/3100%16/16
reports.service.ts +
+
91.74%300/32767.15%227/33891.22%52/5793.24%290/311
startegic-report-insight.service.ts +
+
100%18/1875%9/12100%3/3100%16/16
strategic-report-analytics.service.ts +
+
100%18/1875%9/12100%3/3100%16/16
strategic-reports.service.ts +
+
95.23%280/29470.25%163/23288.7%55/6295.74%270/282
text-answer.service.ts +
+
100%24/2477.77%14/18100%6/6100%20/20
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-analytics.service.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-analytics.service.ts.html new file mode 100644 index 00000000..d441de34 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-analytics.service.ts.html @@ -0,0 +1,238 @@ + + + + + + Code coverage report for reporting/application/services/report-analytics.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services report-analytics.service.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 75% + Branches + 9/12 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +522x +  +  +2x +  +  +  +2x +  +  +2x +  +  +4x +4x +  +  +  +  +  +  +2x +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+import {
+    REPORT_ANALYTICS_REPOSITORY,
+    ReportAnalyticsRepositoryPort,
+} from '../ports/report-analytics.repository.port';
+import { ReportingService } from './reports.service';
+ 
+@Injectable()
+export class ReportAnalyticsService {
+    constructor(
+        @Inject(REPORT_ANALYTICS_REPOSITORY)
+        private readonly analytics: ReportAnalyticsRepositoryPort,
+        private readonly reportingService: ReportingService,
+    ) {}
+ 
+    async getByReportId(
+        reportId: number,
+        actor?: UserDomain,
+    ): Promise<ReportAnalyticsDomain[]> {
+        await this.reportingService.getById(reportId, actor);
+ 
+        const analytics = await this.analytics.findByReportId(reportId);
+ 
+        if (!analytics) {
+            throw new NotFoundException(
+                `Report analytics with report id ${reportId} was not found`,
+            );
+        }
+ 
+        return analytics;
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<ReportAnalyticsDomain> {
+        const analytics = await this.analytics.findById(id);
+ 
+        if (!analytics) {
+            throw new NotFoundException(
+                `Report analytics with id ${id} was not found`,
+            );
+        }
+ 
+        await this.reportingService.getById(analytics.reportId, actor);
+ 
+        return analytics;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-comment.service.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-comment.service.ts.html new file mode 100644 index 00000000..2dc1eb0d --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-comment.service.ts.html @@ -0,0 +1,235 @@ + + + + + + Code coverage report for reporting/application/services/report-comment.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services report-comment.service.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 78.57% + Branches + 11/14 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +512x +  +  +2x +  +  +  +2x +  +  +2x +  +  +6x +6x +  +  +  +  +  +  +3x +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { ReportCommentDomain } from '../../domain/report-comment.domain';
+import {
+    REPORT_COMMENT_REPOSITORY,
+    ReportCommentRepositoryPort,
+} from '../ports/report-comment.repository.port';
+import { ReportingService } from './reports.service';
+ 
+@Injectable()
+export class ReportCommentService {
+    constructor(
+        @Inject(REPORT_COMMENT_REPOSITORY)
+        private readonly comments: ReportCommentRepositoryPort,
+        private readonly reportingService: ReportingService,
+    ) {}
+ 
+    async getByReportId(
+        reportId: number,
+        actor?: UserDomain,
+    ): Promise<ReportCommentDomain[]> {
+        await this.reportingService.getById(reportId, actor);
+ 
+        const comments = await this.comments.findByReportId(reportId);
+ 
+        if (!comments || comments.length === 0) {
+            throw new NotFoundException(
+                `Report comments with report id ${reportId} were not found`,
+            );
+        }
+ 
+        return comments;
+    }
+ 
+    async getById(id: number): Promise<ReportCommentDomain> {
+        const comment = await this.comments.findById(id);
+ 
+        if (!comment) {
+            throw new NotFoundException(
+                `Report comment with id ${id} was not found`,
+            );
+        }
+ 
+        return comment;
+    }
+ 
+    async create(comment: ReportCommentDomain): Promise<ReportCommentDomain> {
+        return this.comments.create(comment);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-insight.service.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-insight.service.ts.html new file mode 100644 index 00000000..8063fd73 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/report-insight.service.ts.html @@ -0,0 +1,238 @@ + + + + + + Code coverage report for reporting/application/services/report-insight.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services report-insight.service.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 75% + Branches + 9/12 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +522x +  +  +2x +  +  +  +2x +  +  +2x +  +  +4x +4x +  +  +  +  +  +  +2x +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { ReportInsightDomain } from '../../domain/report-insight.domain';
+import {
+    REPORT_INSIGHT_REPOSITORY,
+    ReportInsightRepositoryPort,
+} from '../ports/report-insight.repository.port';
+import { ReportingService } from './reports.service';
+ 
+@Injectable()
+export class ReportInsightService {
+    constructor(
+        @Inject(REPORT_INSIGHT_REPOSITORY)
+        private readonly insights: ReportInsightRepositoryPort,
+        private readonly reportingService: ReportingService,
+    ) {}
+ 
+    async getByReportId(
+        reportId: number,
+        actor?: UserDomain,
+    ): Promise<ReportInsightDomain[]> {
+        await this.reportingService.getById(reportId, actor);
+ 
+        const insights = await this.insights.findByReportId(reportId);
+ 
+        if (!insights) {
+            throw new NotFoundException(
+                `Report insights with report id ${reportId} was not found`,
+            );
+        }
+ 
+        return insights;
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<ReportInsightDomain> {
+        const insight = await this.insights.findById(id);
+ 
+        if (!insight) {
+            throw new NotFoundException(
+                `Report insight with id ${id} was not found`,
+            );
+        }
+ 
+        await this.reportingService.getById(insight.reportId, actor);
+ 
+        return insight;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/reports.service.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/reports.service.ts.html new file mode 100644 index 00000000..b9c23aff --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/reports.service.ts.html @@ -0,0 +1,3958 @@ + + + + + + Code coverage report for reporting/application/services/reports.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services reports.service.ts

+
+ +
+ 91.74% + Statements + 300/327 +
+ + +
+ 67.15% + Branches + 227/338 +
+ + +
+ 91.22% + Functions + 52/57 +
+ + +
+ 93.24% + Lines + 290/311 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +1026 +1027 +1028 +1029 +1030 +1031 +1032 +1033 +1034 +1035 +1036 +1037 +1038 +1039 +1040 +1041 +1042 +1043 +1044 +1045 +1046 +1047 +1048 +1049 +1050 +1051 +1052 +1053 +1054 +1055 +1056 +1057 +1058 +1059 +1060 +1061 +1062 +1063 +1064 +1065 +1066 +1067 +1068 +1069 +1070 +1071 +1072 +1073 +1074 +1075 +1076 +1077 +1078 +1079 +1080 +1081 +1082 +1083 +1084 +1085 +1086 +1087 +1088 +1089 +1090 +1091 +1092 +1093 +1094 +1095 +1096 +1097 +1098 +1099 +1100 +1101 +1102 +1103 +1104 +1105 +1106 +1107 +1108 +1109 +1110 +1111 +1112 +1113 +1114 +1115 +1116 +1117 +1118 +1119 +1120 +1121 +1122 +1123 +1124 +1125 +1126 +1127 +1128 +1129 +1130 +1131 +1132 +1133 +1134 +1135 +1136 +1137 +1138 +1139 +1140 +1141 +1142 +1143 +1144 +1145 +1146 +1147 +1148 +1149 +1150 +1151 +1152 +1153 +1154 +1155 +1156 +1157 +1158 +1159 +1160 +1161 +1162 +1163 +1164 +1165 +1166 +1167 +1168 +1169 +1170 +1171 +1172 +1173 +1174 +1175 +1176 +1177 +1178 +1179 +1180 +1181 +1182 +1183 +1184 +1185 +1186 +1187 +1188 +1189 +1190 +1191 +1192 +1193 +1194 +1195 +1196 +1197 +1198 +1199 +1200 +1201 +1202 +1203 +1204 +1205 +1206 +1207 +1208 +1209 +1210 +1211 +1212 +1213 +1214 +1215 +1216 +1217 +1218 +1219 +1220 +1221 +1222 +1223 +1224 +1225 +1226 +1227 +1228 +1229 +1230 +1231 +1232 +1233 +1234 +1235 +1236 +1237 +1238 +1239 +1240 +1241 +1242 +1243 +1244 +1245 +1246 +1247 +1248 +1249 +1250 +1251 +1252 +1253 +1254 +1255 +1256 +1257 +1258 +1259 +1260 +1261 +1262 +1263 +1264 +1265 +1266 +1267 +1268 +1269 +1270 +1271 +1272 +1273 +1274 +1275 +1276 +1277 +1278 +1279 +1280 +1281 +1282 +1283 +1284 +1285 +1286 +1287 +1288 +1289 +1290 +1291 +12926x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x +  +  +  +  +  +  +6x +6x +  +  +  +6x +  +  +  +  +6x +  +  +  +6x +  +  +  +6x +  +  +  +6x +  +  +  +6x +  +  +  +  +6x +  +  +  +6x +  +  +  +  +6x +6x +6x +6x +  +  +  +6x +  +  +  +6x +  +  +  +6x +  +  +  +  +  +6x +15x +  +  +  +15x +  +15x +  +15x +  +15x +  +15x +  +15x +  +15x +  +15x +  +15x +  +15x +  +15x +  +15x +  +  +15x +  +  +  +  +  +3x +2x +  +  +  +  +  +  +  +3x +  +  +2x +  +  +2x +1x +  +  +  +  +  +  +6x +  +6x +1x +  +  +5x +  +3x +  +  +  +  +  +  +  +  +  +  +  +6x +  +  +5x +  +  +5x +  +4x +  +4x +1x +  +  +  +3x +3x +  +3x +  +2x +  +  +  +2x +  +1x +  +  +  +  +  +  +  +  +2x +2x +1x +  +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +4x +  +4x +4x +1x +1x +  +  +3x +3x +1x +  +  +2x +  +  +  +2x +2x +  +  +1x +  +  +  +  +  +  +  +  +2x +  +2x +2x +2x +  +  +2x +1x +  +  +1x +  +1x +1x +  +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +1x +  +1x +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +1x +  +  +  +  +1x +  +  +  +  +  +  +1x +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +1x +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +1x +  +  +  +  +  +  +1x +1x +2x +  +  +1x +1x +1x +1x +  +  +  +  +1x +1x +  +  +1x +1x +1x +  +1x +  +  +  +1x +1x +  +1x +1x +  +1x +  +  +  +1x +  +  +  +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +  +  +1x +1x +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +16x +13x +11x +16x +11x +11x +  +  +11x +  +  +  +  +  +  +  +  +  +  +48x +  +42x +42x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +  +  +2x +1x +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +1x +  +  +  +1x +1x +1x +1x +1x +  +1x +  +1x +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +1x +  +1x +1x +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +1x +1x +  +1x +1x +  +1x +  +  +  +1x +  +  +  +1x +1x +1x +1x +  +1x +1x +1x +  +1x +1x +  +1x +  +  +  +1x +1x +  +1x +1x +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +1x +  +1x +  +1x +1x +2x +  +  +2x +2x +2x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +6x +  +  +  +2x +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +10x +5x +  +5x +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +1x +1x +  +1x +1x +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +15x +5x +  +10x +  +  +  +  +  +  +  +  +24x +15x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +2x +  +2x +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +2x +2x +2x +2x +  +  +2x +  +2x +  +2x +  +2x +  +2x +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +2x +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +  +  +15x +6x +12x +  +6x +6x +  +  +  +  +  +  +  +  +6x +6x +6x +  +6x +  +  + 
import {
+    AnswerType,
+    CompetenceAccumulator,
+    CYCLE_CONSTRAINTS,
+    EntityAverageInsightsDto,
+    EntityInsightSummaryDto,
+    EntitySummaryTotals,
+    EntityType,
+    IdentityRole,
+    InsightType,
+    REPORT_ANALYTICS_CONSTRAINTS,
+    ReportSearchQuery,
+    RESPONDENT_CATEGORIES,
+    RespondentCategory,
+} from '@intra/shared-kernel';
+import {
+    ForbiddenException,
+    Inject,
+    Injectable,
+    Logger,
+    NotFoundException,
+} from '@nestjs/common';
+import Decimal from 'decimal.js';
+import {
+    CYCLE_REPOSITORY,
+    CycleRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/cycle.repository.port';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/reviewer.repository.port';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../../../feedback360/application/ports/answer.repository.port';
+import {
+    CLUSTER_SCORE_REPOSITORY,
+    ClusterScoreRepositoryPort,
+} from '../../../feedback360/application/ports/cluster-score.repository.port';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../../../feedback360/application/ports/respondent.repository.port';
+import {
+    REVIEW_QUESTION_RELATION_REPOSITORY,
+    ReviewQuestionRelationRepositoryPort,
+} from '../../../feedback360/application/ports/review-question-relation.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../../../feedback360/application/ports/review.repository.port';
+import { AnswerDomain } from '../../../feedback360/domain/answer.domain';
+import { ClusterScoreDomain } from '../../../feedback360/domain/cluster-score.domain';
+import { RespondentDomain } from '../../../feedback360/domain/respondent.domain';
+import { ReviewQuestionRelationDomain } from '../../../feedback360/domain/review-question-relation.domain';
+import { ReviewDomain } from '../../../feedback360/domain/review.domain';
+import {
+    CLUSTER_REPOSITORY,
+    ClusterRepositoryPort,
+} from '../../../library/application/ports/cluster.repository.port';
+import { ClusterDomain } from '../../../library/domain/cluster.domain';
+import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+import { ReportInsightDomain } from '../../domain/report-insight.domain';
+import { ReportDomain } from '../../domain/report.domain';
+import {
+    REPORT_ANALYTICS_REPOSITORY,
+    ReportAnalyticsRepositoryPort,
+} from '../ports/report-analytics.repository.port';
+import {
+    REPORT_COMMENT_REPOSITORY,
+    ReportCommentRepositoryPort,
+} from '../ports/report-comment.repository.port';
+import {
+    REPORT_INSIGHT_REPOSITORY,
+    ReportInsightRepositoryPort,
+} from '../ports/report-insight.repository.port';
+import {
+    REPORT_REPOSITORY,
+    ReportRepositoryPort,
+} from '../ports/report.repository.port';
+ 
+@Injectable()
+export class ReportingService {
+    private readonly logger = new Logger(ReportingService.name);
+ 
+    constructor(
+        @Inject(REPORT_REPOSITORY)
+        private readonly reports: ReportRepositoryPort,
+        @Inject(REPORT_ANALYTICS_REPOSITORY)
+        private readonly analyticsRepo: ReportAnalyticsRepositoryPort,
+        @Inject(REPORT_INSIGHT_REPOSITORY)
+        private readonly insightsRepo: ReportInsightRepositoryPort,
+        @Inject(REPORT_COMMENT_REPOSITORY)
+        private readonly commentsRepo: ReportCommentRepositoryPort,
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        @Inject(REVIEWER_REPOSITORY)
+        private readonly reviewers: ReviewerRepositoryPort,
+        @Inject(ANSWER_REPOSITORY)
+        private readonly answers: AnswerRepositoryPort,
+        @Inject(REVIEW_QUESTION_RELATION_REPOSITORY)
+        private readonly reviewQuestionRelations: ReviewQuestionRelationRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        @Inject(CLUSTER_REPOSITORY)
+        private readonly clusters: ClusterRepositoryPort,
+        @Inject(CLUSTER_SCORE_REPOSITORY)
+        private readonly clusterScores: ClusterScoreRepositoryPort,
+        @Inject(CYCLE_REPOSITORY)
+        private readonly cyclesRepo: CycleRepositoryPort,
+    ) {}
+ 
+    private readonly isProduction = process.env.NODE_ENV === 'production';
+ 
+    async search(
+        query: ReportSearchQuery,
+        actor?: UserDomain,
+    ): Promise<ReportDomain[]> {
+        await this.checkAccessToAllReports(actor);
+        return await this.reports.search(query);
+    }
+ 
+    /**
+     * Checks if the actor has access to all reports (HR or Admin only).
+     * @param actor The actor to check access for.
+     */
+    async checkAccessToAllReports(actor?: UserDomain): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (!isAdminOrHr) {
+            throw new ForbiddenException(
+                'You do not have permission to view this report',
+            );
+        }
+    }
+ 
+    async getById(id: number, actor?: UserDomain): Promise<ReportDomain> {
+        const report = await this.reports.findById(id);
+ 
+        if (!report) {
+            throw new NotFoundException(`Report with id ${id} was not found`);
+        }
+ 
+        await this.checkAccessToReport(report, actor);
+ 
+        return report;
+    }
+ 
+    /**
+     * Checks if the actor has access to the report.
+     * @param report The report to check access for.
+     * @param actor The actor to check access for.
+     */
+    async checkAccessToReport(
+        report: ReportDomain,
+        actor?: UserDomain,
+    ): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const review = await this.reviews.findById(report.reviewId);
+ 
+        if (!review) {
+            throw new NotFoundException(`Review for report ${report.id} was not found. 
+                Unable to check access to report`);
+        }
+ 
+        const isManagerOfReview = review.managerId === actor.id;
+        const isRateeOfReview = review.rateeId === actor.id;
+ 
+        if (isManagerOfReview || isRateeOfReview) return;
+ 
+        const reviewers = await this.reviewers.listByReview(review.id!, {
+            reviewerId: actor.id,
+        });
+ 
+        if (reviewers.length > 0) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view this report',
+        );
+    }
+ 
+    async getByReviewId(
+        reviewId: number,
+        actor?: UserDomain,
+    ): Promise<ReportDomain> {
+        const report = await this.reports.findByReviewId(reviewId);
+        if (!report) {
+            throw new NotFoundException(
+                `Report for review ${reviewId} was not found`,
+            );
+        }
+ 
+        await this.checkAccessToReport(report, actor);
+ 
+        return report;
+    }
+ 
+    /**
+     * Generates a comprehensive report for a review if anonymity threshold is met.
+     * Calculates respondent counts and derives analytics from answers.
+     * @param reviewId The ID of the review to generate a report for.
+     * @returns The generated report.
+     */
+    async generateReportForReview(reviewId: number): Promise<ReportDomain> {
+        this.logger.debug(`Started report generation for review ${reviewId}`);
+ 
+        const existing = await this.reports.findByReviewId(reviewId);
+        if (existing) {
+            this.logger.debug(`Report already exists for review ${reviewId}`);
+            return existing;
+        }
+ 
+        const review = await this.reviews.findById(reviewId);
+        if (!review) {
+            throw new NotFoundException(`Review with id ${reviewId} not found`);
+        }
+ 
+        const allRespondents = await this.respondents.listByReview(
+            reviewId,
+            {},
+        );
+        const respondentCount = allRespondents.length;
+        const respondentCategories: RespondentCategory[] = [
+            ...new Set(
+                allRespondents
+                    .map((r) => r.category)
+                    .sort(
+                        (a, b) =>
+                            RESPONDENT_CATEGORIES.indexOf(a) -
+                            RESPONDENT_CATEGORIES.indexOf(b),
+                    ),
+            ),
+        ];
+        const answersCounts =
+            await this.answers.getAnswersCountByRespondentCategories(reviewId);
+ 
+        let answerCount = 0;
+        answersCounts.forEach((stat) => {
+            answerCount += stat.answers;
+        });
+ 
+        if (this.isProduction) {
+            await this.verifyAnonimityThreshold(review, answersCounts);
+        }
+ 
+        const actualAnswersByCategory = answersCounts.reduce(
+            (acc, stat) => {
+                acc[stat.respondentCategory] = stat.answers;
+                return acc;
+            },
+            {} as Record<string, number>,
+        );
+ 
+        const teamTurnout = this.calculateTurnout(
+            allRespondents,
+            actualAnswersByCategory[RespondentCategory.TEAM] || 0,
+            RespondentCategory.TEAM,
+        );
+        const otherTurnout = this.calculateTurnout(
+            allRespondents,
+            actualAnswersByCategory[RespondentCategory.OTHER] || 0,
+            RespondentCategory.OTHER,
+        );
+ 
+        this.logger.debug(
+            `Calculated respondent counts: ${respondentCount} total in ${respondentCategories.join(', ')} categories, answer count ${answerCount}, team turnout ${teamTurnout}, other turnout ${otherTurnout}`,
+        );
+ 
+        const answers = await this.answers.list({
+            reviewId,
+            answerType: AnswerType.NUMERICAL_SCALE,
+        });
+ 
+        const relations = await this.reviewQuestionRelations.listByReview(
+            reviewId,
+            {},
+        );
+ 
+        const maxScore = new Decimal(REPORT_ANALYTICS_CONSTRAINTS.SCORE.MAX);
+        const { questionAnalytics, competenceAnalytics, questionTotals } =
+            this.buildAnalyticsPayload(answers, relations, maxScore);
+ 
+        const competenceTotals = this.calculateCompetenceSummaryTotals(
+            competenceAnalytics,
+            maxScore,
+        );
+ 
+        const report = ReportDomain.create({
+            reviewId,
+            cycleId: review.cycleId,
+            respondentCount,
+            respondentCategories,
+            answerCount,
+            turnoutPctOfTeam: teamTurnout,
+            turnoutPctOfOther: otherTurnout,
+            questionTotAvgBySelf: questionTotals.averageBySelfAssessment,
+            questionTotAvgByTeam: questionTotals.averageByTeam,
+            questionTotAvgByOthers: questionTotals.averageByOther,
+            questionTotPctBySelf: questionTotals.percentageBySelfAssessment,
+            questionTotPctByTeam: questionTotals.percentageByTeam,
+            questionTotPctByOthers: questionTotals.percentageByOther,
+            questionTotDeltaPctByTeam: questionTotals.deltaPercentageByTeam,
+            questionTotDeltaPctByOthers: questionTotals.deltaPercentageByOther,
+            competenceTotAvgBySelf: competenceTotals.averageBySelfAssessment,
+            competenceTotAvgByTeam: competenceTotals.averageByTeam,
+            competenceTotAvgByOthers: competenceTotals.averageByOther,
+            competenceTotPctBySelf: competenceTotals.percentageBySelfAssessment,
+            competenceTotPctByTeam: competenceTotals.percentageByTeam,
+            competenceTotPctByOthers: competenceTotals.percentageByOther,
+            competenceTotDeltaPctByTeam: competenceTotals.deltaPercentageByTeam,
+            competenceTotDeltaPctByOthers:
+                competenceTotals.deltaPercentageByOther,
+            analytics: [],
+            comments: [],
+        });
+ 
+        const createdReport = await this.reports.create(report);
+ 
+        await this.analyticsRepo.createMany(createdReport.id!, [
+            ...questionAnalytics,
+            ...competenceAnalytics,
+        ]);
+ 
+        await this.saveClusterScoresForCompetences(
+            competenceAnalytics,
+            answers,
+            relations,
+            review,
+        );
+ 
+        this.logger.debug(
+            `Generated ${questionAnalytics.length} question analytics and ${competenceAnalytics.length} competence analytics`,
+        );
+ 
+        const questionInsights = await this.calculateEntityInsights(
+            questionAnalytics,
+            EntityType.QUESTION,
+            createdReport.id!,
+        );
+ 
+        const competenceInsights = await this.calculateEntityInsights(
+            competenceAnalytics,
+            EntityType.COMPETENCE,
+            createdReport.id!,
+        );
+ 
+        await this.insightsRepo.createMany(createdReport.id!, [
+            ...questionInsights,
+            ...competenceInsights,
+        ]);
+ 
+        this.logger.debug(
+            `Generated ${questionInsights.length} question insights and ${competenceInsights.length} competence insights`,
+        );
+ 
+        this.logger.debug(
+            `Successfully created report ${createdReport.id} for review ${reviewId}`,
+        );
+ 
+        await this.reviews.updateById(reviewId, {
+            reportId: createdReport.id,
+        });
+ 
+        return createdReport;
+    }
+ 
+    /**
+     * Verifies that the anonymity threshold is met for the given review.
+     * Anonymity threshold is the minimum number of answers
+     * required for a review to be valid for report generation.
+     * If the threshold is not met, an exception is thrown.
+     * @param review The review.
+     * @param answers The actual answers.
+     */
+    private async verifyAnonimityThreshold(
+        review: ReviewDomain,
+        answersCounts: {
+            respondentCategory: RespondentCategory;
+            answers: number;
+        }[],
+    ) {
+        let anonimityThreshold;
+ 
+        if (review.cycleId) {
+            const cycle = await this.cyclesRepo.findById(review.cycleId);
+            anonimityThreshold = cycle?.minRespondentsThreshold;
+        } else E{
+            anonimityThreshold = CYCLE_CONSTRAINTS.ANONYMITY_THRESHOLD.MIN;
+        }
+ 
+        let isAnonimityThresholdMet: {
+            category: RespondentCategory;
+            met: boolean;
+        }[] = [];
+ 
+        Eif (
+            answersCounts.length === 1 &&
+            answersCounts[0].respondentCategory ===
+                RespondentCategory.SELF_ASSESSMENT
+        ) {
+            throw new NotFoundException(
+                `Not enough answers in category to meet the anonymity threshold ${anonimityThreshold} for review ${review.id}: only self assessment answers available.`,
+            );
+        }
+ 
+        answersCounts.forEach((stat) => {
+            if (
+                stat.respondentCategory === RespondentCategory.SELF_ASSESSMENT
+            ) {
+                return;
+            }
+ 
+            isAnonimityThresholdMet.push({
+                category: stat.respondentCategory,
+                met: stat.answers >= anonimityThreshold,
+            });
+        });
+ 
+        if (isAnonimityThresholdMet.some((item) => !item.met)) {
+            throw new NotFoundException(
+                `Not enough answers in category to meet the anonymity threshold ${anonimityThreshold} for review ${review.id}: ${JSON.stringify(isAnonimityThresholdMet)}`,
+            );
+        }
+    }
+ 
+    /**
+     * Saves the cluster scores for the competences.
+     * @param competenceAnalytics The competence analytics.
+     * @param answers The answers.
+     * @param relations The review-question relations.
+     * @param review The review.
+     */
+    private async saveClusterScoresForCompetences(
+        competenceAnalytics: ReportAnalyticsDomain[],
+        answers: AnswerDomain[],
+        relations: ReviewQuestionRelationDomain[],
+        review: ReviewDomain,
+    ): Promise<void> {
+        const questionToCompetence = new Map<number, number>();
+        for (const relation of relations) {
+            Eif (
+                relation.answerType === AnswerType.NUMERICAL_SCALE &&
+                relation.competenceId !== null &&
+                relation.competenceId !== undefined
+            ) {
+                questionToCompetence.set(
+                    relation.questionId,
+                    relation.competenceId,
+                );
+            }
+        }
+ 
+        const competenceAnswerCounts = new Map<number, number>();
+        for (const answer of answers) {
+            if (
+                answer.respondentCategory === RespondentCategory.SELF_ASSESSMENT
+            )
+                continue;
+            const competenceId = questionToCompetence.get(answer.questionId);
+            Iif (competenceId === undefined) continue;
+            Iif (
+                answer.numericalValue === null ||
+                answer.numericalValue === undefined
+            )
+                continue;
+            const current = competenceAnswerCounts.get(competenceId) ?? 0;
+            competenceAnswerCounts.set(competenceId, current + 1);
+        }
+ 
+        for (const analytic of competenceAnalytics) {
+            const competenceId = analytic.competenceId;
+            Iif (competenceId === null || competenceId === undefined) continue;
+ 
+            const finalScore = this.calculateCompetenceFinalScore(
+                analytic.averageByTeam ?? null,
+                analytic.averageByOther ?? null,
+            );
+            const roundedScore = this.roundDecimal(finalScore);
+            Iif (roundedScore === null) continue;
+ 
+            const clusters = await this.clusters.search({ competenceId });
+            Iif (!clusters.length) continue;
+ 
+            const matchedCluster = this.findMatchingCluster(
+                roundedScore,
+                clusters,
+            );
+            Iif (!matchedCluster) {
+                this.logger.debug(
+                    `Cluster not found for competence ${competenceId} and score ${roundedScore.toFixed(4)}`,
+                );
+                continue;
+            }
+ 
+            const answersCount = competenceAnswerCounts.get(competenceId) ?? 0;
+ 
+            await this.clusterScores.upsert(
+                ClusterScoreDomain.create({
+                    cycleId: review.cycleId ?? null,
+                    clusterId: matchedCluster.id!,
+                    rateeId: review.rateeId,
+                    reviewId: review.id!,
+                    score: roundedScore,
+                    answersCount,
+                }),
+            );
+        }
+    }
+ 
+    /**
+     * Calculates the final score for a competence.
+     * @param averageByTeam The average score by team.
+     * @param averageByOther The average score by other.
+     * @returns The final score.
+     */
+    private calculateCompetenceFinalScore(
+        averageByTeam: Decimal | null,
+        averageByOther: Decimal | null,
+    ): Decimal | null {
+        return this.calculateAverage([averageByTeam, averageByOther]);
+    }
+ 
+    /**
+     * Finds the matching cluster for a score.
+     * @param score The score to find the cluster for.
+     * @param clusters The clusters to search in.
+     * @returns The matching cluster.
+     */
+    private findMatchingCluster(
+        score: Decimal,
+        clusters: ClusterDomain[],
+    ): ClusterDomain | null {
+        for (const cluster of clusters) {
+            const lower = new Decimal(cluster.lowerBound);
+            const upper = new Decimal(cluster.upperBound);
+            const isMaxUpper = upper.equals(
+                new Decimal(REPORT_ANALYTICS_CONSTRAINTS.SCORE.MAX),
+            );
+            const meetsLower = score.greaterThanOrEqualTo(lower);
+            const meetsUpper = isMaxUpper
+                ? score.lessThanOrEqualTo(upper)
+                : score.lessThan(upper);
+            Eif (meetsLower && meetsUpper) {
+                return cluster;
+            }
+        }
+        return null;
+    }
+ 
+    /**
+     * Calculates average of decimals, returns null if array is empty.
+     * Formula: sum / count
+     * @param numbers The array of numbers to calculate the average of.
+     * @returns The average of the numbers as a Decimal, or null if the array is empty.
+     */
+    private calculateAverage(
+        numbers: (Decimal | number | null | undefined)[],
+    ): Decimal | null {
+        const validNumbers = numbers
+            .filter((n): n is Decimal | number => n !== null && n !== undefined)
+            .map((n) => (n instanceof Decimal ? n : new Decimal(n)));
+        if (validNumbers.length === 0) return null;
+        const sum = validNumbers.reduce(
+            (acc, value) => acc.plus(value),
+            new Decimal(0),
+        );
+        return sum.dividedBy(validNumbers.length);
+    }
+ 
+    /**
+     * Normalizes decimal scale to 4 digits (or null)
+     * @param value The value to normalize.
+     * @returns The normalized value.
+     */
+    private roundDecimal(
+        value: Decimal | number | null | undefined,
+    ): Decimal | null {
+        if (value === null || value === undefined) return null;
+        const decimalValue =
+            value instanceof Decimal ? value : new Decimal(value);
+        return decimalValue.toDecimalPlaces(4);
+    }
+ 
+    /**
+     * Calculates the turnout percentage for a specific category of respondents.
+     * Formula: (actual answers / assigned respondents) * 100
+     * @param respondents The list of all respondents.
+     * @param actualAnswers The count of actual answers for the category.
+     * @param category The category to calculate turnout for.
+     * @returns The turnout percentage as a Decimal, or null if no respondents in category.
+     */
+    private calculateTurnout(
+        respondents: RespondentDomain[],
+        actualAnswers: number,
+        category: RespondentCategory,
+    ): Decimal | null {
+        const assignedRespondents = respondents.filter(
+            (respondent) => respondent.category === category,
+        );
+ 
+        if (assignedRespondents.length === 0) {
+            return null;
+        }
+ 
+        Iif (actualAnswers === 0) {
+            return new Decimal(0);
+        }
+ 
+        const turnout = new Decimal(actualAnswers)
+            .dividedBy(assignedRespondents.length)
+            .times(100);
+ 
+        return this.roundDecimal(turnout);
+    }
+ 
+    /**
+     * Builds the analytics payload for a report, calculating averages and percentages for questions and competences.
+     * @param answers The answers to calculate analytics from.
+     * @param relations The review-question relations to determine allowed question IDs.
+     * @param maxScore The maximum score for the report.
+     * @returns An object containing question analytics, competence analytics, and summary totals.
+     */
+    private buildAnalyticsPayload(
+        answers: AnswerDomain[],
+        relations: ReviewQuestionRelationDomain[],
+        maxScore: Decimal,
+    ): {
+        questionAnalytics: ReportAnalyticsDomain[];
+        competenceAnalytics: ReportAnalyticsDomain[];
+        questionTotals: EntitySummaryTotals;
+        competenceTotals: EntitySummaryTotals;
+    } {
+        const numericalRelations = relations.filter(
+            (r) => r.answerType === AnswerType.NUMERICAL_SCALE,
+        );
+ 
+        const answersByQuestion = this.groupAnswersByQuestion(
+            answers,
+            numericalRelations,
+        );
+        const questionAnalytics: ReportAnalyticsDomain[] = [];
+        const competenceAccumulators = new Map<number, CompetenceAccumulator>();
+        const questionSelfAverages: Decimal[] = [];
+        const questionTeamAverages: Decimal[] = [];
+        const questionOtherAverages: Decimal[] = [];
+ 
+        for (const relation of numericalRelations) {
+            const questionAnswers =
+                answersByQuestion.get(relation.questionId) ?? [];
+ 
+            const averageBySelf = this.calculateAverageByCategory(
+                questionAnswers,
+                RespondentCategory.SELF_ASSESSMENT,
+            );
+            const averageByTeam = this.calculateAverageByCategory(
+                questionAnswers,
+                RespondentCategory.TEAM,
+            );
+            const averageByOther = this.calculateAverageByCategory(
+                questionAnswers,
+                RespondentCategory.OTHER,
+            );
+ 
+            Eif (averageBySelf !== null) {
+                questionSelfAverages.push(averageBySelf);
+            }
+            Eif (averageByTeam !== null) {
+                questionTeamAverages.push(averageByTeam);
+            }
+            Iif (averageByOther !== null) {
+                questionOtherAverages.push(averageByOther);
+            }
+ 
+            const percentageBySelf = this.calculatePercentage(
+                averageBySelf,
+                maxScore,
+            );
+            const percentageByTeam = this.calculatePercentage(
+                averageByTeam,
+                maxScore,
+            );
+            const percentageByOther = this.calculatePercentage(
+                averageByOther,
+                maxScore,
+            );
+ 
+            const deltaPercentageByTeam = this.calculateDeltaRating(
+                percentageByTeam,
+                percentageBySelf,
+            );
+            const deltaPercentageByOther = this.calculateDeltaRating(
+                percentageByOther,
+                percentageBySelf,
+            );
+ 
+            questionAnalytics.push(
+                ReportAnalyticsDomain.create({
+                    reportId: 0,
+                    entityType: EntityType.QUESTION,
+                    questionId: relation.questionId,
+                    questionTitle: relation.questionTitle,
+                    competenceId: null,
+                    competenceTitle: null,
+                    averageBySelfAssessment:
+                        this.roundDecimal(averageBySelf) ?? null,
+                    averageByTeam: this.roundDecimal(averageByTeam) ?? null,
+                    averageByOther: this.roundDecimal(averageByOther) ?? null,
+                    percentageBySelfAssessment:
+                        this.roundDecimal(percentageBySelf) ?? null,
+                    percentageByTeam:
+                        this.roundDecimal(percentageByTeam) ?? null,
+                    percentageByOther:
+                        this.roundDecimal(percentageByOther) ?? null,
+                    deltaPercentageByTeam:
+                        this.roundDecimal(deltaPercentageByTeam) ?? null,
+                    deltaPercentageByOther:
+                        this.roundDecimal(deltaPercentageByOther) ?? null,
+                }),
+            );
+ 
+            const competenceId = relation.competenceId;
+            Eif (competenceId !== null && competenceId !== undefined) {
+                const accumulator = competenceAccumulators.get(
+                    competenceId,
+                ) ?? {
+                    competenceId,
+                    competenceTitle: relation.competenceTitle ?? null,
+                    selfScores: [],
+                    teamScores: [],
+                    otherScores: [],
+                };
+ 
+                Eif (averageBySelf !== null) {
+                    accumulator.selfScores.push(averageBySelf);
+                }
+                Eif (averageByTeam !== null) {
+                    accumulator.teamScores.push(averageByTeam);
+                }
+                Iif (averageByOther !== null) {
+                    accumulator.otherScores.push(averageByOther);
+                }
+ 
+                competenceAccumulators.set(competenceId, accumulator);
+            }
+        }
+ 
+        const competenceAnalytics: ReportAnalyticsDomain[] = [];
+        const competenceSelfAverages: Decimal[] = [];
+        const competenceTeamAverages: Decimal[] = [];
+        const competenceOtherAverages: Decimal[] = [];
+ 
+        for (const accumulator of competenceAccumulators.values()) {
+            const averageBySelf = this.calculateAverage(
+                accumulator.selfScores.map((s) => new Decimal(s)),
+            );
+            const averageByTeam = this.calculateAverage(
+                accumulator.teamScores.map((s) => new Decimal(s)),
+            );
+            const averageByOther = this.calculateAverage(
+                accumulator.otherScores.map((s) => new Decimal(s)),
+            );
+ 
+            Eif (averageBySelf !== null) {
+                competenceSelfAverages.push(averageBySelf);
+            }
+            Eif (averageByTeam !== null) {
+                competenceTeamAverages.push(averageByTeam);
+            }
+            Iif (averageByOther !== null) {
+                competenceOtherAverages.push(averageByOther);
+            }
+ 
+            const percentageBySelf = this.calculatePercentage(
+                averageBySelf,
+                maxScore,
+            );
+            const percentageByTeam = this.calculatePercentage(
+                averageByTeam,
+                maxScore,
+            );
+            const percentageByOther = this.calculatePercentage(
+                averageByOther,
+                maxScore,
+            );
+ 
+            const deltaPercentageByTeam = this.calculateDeltaRating(
+                percentageByTeam,
+                percentageBySelf,
+            );
+            const deltaPercentageByOther = this.calculateDeltaRating(
+                percentageByOther,
+                percentageBySelf,
+            );
+ 
+            competenceAnalytics.push(
+                ReportAnalyticsDomain.create({
+                    reportId: 0,
+                    entityType: EntityType.COMPETENCE,
+                    questionId: null,
+                    questionTitle: null,
+                    competenceId: accumulator.competenceId,
+                    competenceTitle: accumulator.competenceTitle,
+                    averageBySelfAssessment:
+                        this.roundDecimal(averageBySelf) ?? null,
+                    averageByTeam: this.roundDecimal(averageByTeam) ?? null,
+                    averageByOther: this.roundDecimal(averageByOther) ?? null,
+                    percentageBySelfAssessment:
+                        this.roundDecimal(percentageBySelf) ?? null,
+                    percentageByTeam:
+                        this.roundDecimal(percentageByTeam) ?? null,
+                    percentageByOther:
+                        this.roundDecimal(percentageByOther) ?? null,
+                    deltaPercentageByTeam:
+                        this.roundDecimal(deltaPercentageByTeam) ?? null,
+                    deltaPercentageByOther:
+                        this.roundDecimal(deltaPercentageByOther) ?? null,
+                }),
+            );
+        }
+ 
+        return {
+            questionAnalytics,
+            competenceAnalytics,
+            questionTotals: this.calculateSummaryTotalsFromValues(
+                questionSelfAverages,
+                questionTeamAverages,
+                questionOtherAverages,
+                maxScore,
+            ),
+            competenceTotals: this.calculateSummaryTotalsFromValues(
+                competenceSelfAverages,
+                competenceTeamAverages,
+                competenceOtherAverages,
+                maxScore,
+            ),
+        };
+    }
+ 
+    /**
+     * Groups answers by question ID, filtering for numerical scale answers only.
+     * @param answers The answers to group.
+     * @param relations The review-question relations to determine allowed question IDs.
+     * @returns A map of question IDs to arrays of answers.
+     */
+    private groupAnswersByQuestion(
+        answers: AnswerDomain[],
+        relations: ReviewQuestionRelationDomain[],
+    ): Map<number, AnswerDomain[]> {
+        const allowedQuestionIds = new Set(
+            relations
+                .filter(
+                    (relation) =>
+                        relation.answerType === AnswerType.NUMERICAL_SCALE,
+                )
+                .map((relation) => relation.questionId),
+        );
+        const grouped = new Map<number, AnswerDomain[]>();
+        for (const answer of answers) {
+            Iif (!allowedQuestionIds.has(answer.questionId)) {
+                continue;
+            }
+            const bucket = grouped.get(answer.questionId) ?? [];
+            bucket.push(answer);
+            grouped.set(answer.questionId, bucket);
+        }
+        return grouped;
+    }
+ 
+    /**
+     * Calculates the average score for a specific category of respondents.
+     * @param answers The answers to calculate the average from.
+     * @param category The category of respondents.
+     * @returns The average score for the category.
+     */
+    private calculateAverageByCategory(
+        answers: AnswerDomain[],
+        category: RespondentCategory,
+    ): Decimal | null {
+        const values = answers
+            .filter(
+                (answer) =>
+                    answer.respondentCategory === category &&
+                    answer.numericalValue !== null &&
+                    answer.numericalValue !== undefined,
+            )
+            .map((answer) => new Decimal(answer.numericalValue!));
+        return this.calculateAverage(values);
+    }
+ 
+    /**
+     * Calculates the difference between two values in percentage points.
+     * Formula: respondent rating - self rating.
+     * @param respondent The respondent rating.
+     * @param self The self assessment rating.
+     * @returns The difference in percentage points.
+     */
+    private calculateDeltaRating(
+        respondent: Decimal | null,
+        self: Decimal | null,
+    ): Decimal | null {
+        if (respondent === null || self === null) {
+            return null;
+        }
+        return this.roundDecimal(respondent.minus(self));
+    }
+ 
+    /**
+     * Calculates the summary totals for a competence.
+     * @param analytics The analytics for the competence.
+     * @param maxScore The maximum score.
+     * @returns The summary totals.
+     */
+    private calculateCompetenceSummaryTotals(
+        analytics: ReportAnalyticsDomain[],
+        maxScore: Decimal,
+    ): EntitySummaryTotals {
+        const averageBySelf = this.calculateAverage(
+            analytics.map((item) => item.averageBySelfAssessment),
+        );
+        const averageByTeam = this.calculateAverage(
+            analytics.map((item) => item.averageByTeam),
+        );
+        const averageByOther = this.calculateAverage(
+            analytics.map((item) => item.averageByOther),
+        );
+ 
+        const percentageBySelf = this.calculatePercentage(
+            averageBySelf,
+            maxScore,
+        );
+        const percentageByTeam = this.calculatePercentage(
+            averageByTeam,
+            maxScore,
+        );
+        const percentageByOther = this.calculatePercentage(
+            averageByOther,
+            maxScore,
+        );
+ 
+        const deltaPercentageByTeam = this.calculateDeltaRating(
+            percentageByTeam,
+            percentageBySelf,
+        );
+        const deltaPercentageByOther = this.calculateDeltaRating(
+            percentageByOther,
+            percentageBySelf,
+        );
+        return {
+            averageBySelfAssessment: this.toRoundedNumber(averageBySelf),
+            averageByTeam: this.toRoundedNumber(averageByTeam),
+            averageByOther: this.toRoundedNumber(averageByOther),
+            percentageBySelfAssessment: this.toRoundedNumber(percentageBySelf),
+            percentageByTeam: this.toRoundedNumber(percentageByTeam),
+            percentageByOther: this.toRoundedNumber(percentageByOther),
+            deltaPercentageByTeam: this.toRoundedNumber(deltaPercentageByTeam),
+            deltaPercentageByOther: this.toRoundedNumber(
+                deltaPercentageByOther,
+            ),
+        };
+    }
+ 
+    /**
+     * Calculates the percentage of a value relative to a maximum score.
+     * Formula: (value / maxScore) * 100
+     * @param value The value to calculate the percentage of.
+     * @param maxScore The maximum score.
+     * @returns The percentage.
+     */
+    private calculatePercentage(
+        value: Decimal | null,
+        maxScore: Decimal,
+    ): Decimal | null {
+        if (value === null || maxScore.isZero()) {
+            return null;
+        }
+        return this.roundDecimal(value.dividedBy(maxScore).times(100));
+    }
+ 
+    /**
+     * Rounds a Decimal value to 4 decimal places.
+     * @param value The value to round.
+     * @returns The rounded value.
+     */
+    private toRoundedNumber(value: Decimal | null): number | null {
+        if (value === null) return null;
+        return Number(this.roundDecimal(value)?.toFixed(4));
+    }
+ 
+    /**
+     * Calculates the summary totals from arrays of values.
+     * @param selfValues The values for self-assessment.
+     * @param teamValues The values for team assessment.
+     * @param otherValues The values for other assessment.
+     * @param maxScore The maximum score.
+     * @returns The summary totals.
+     */
+    private calculateSummaryTotalsFromValues(
+        selfValues: Decimal[],
+        teamValues: Decimal[],
+        otherValues: Decimal[],
+        maxScore: Decimal,
+    ): EntitySummaryTotals {
+        const averageBySelf = this.calculateAverage(selfValues);
+        const averageByTeam = this.calculateAverage(teamValues);
+        const averageByOther = this.calculateAverage(otherValues);
+ 
+        const percentageBySelf = this.calculatePercentage(
+            averageBySelf,
+            maxScore,
+        );
+        const percentageByTeam = this.calculatePercentage(
+            averageByTeam,
+            maxScore,
+        );
+        const percentageByOther = this.calculatePercentage(
+            averageByOther,
+            maxScore,
+        );
+ 
+        const deltaPercentageByTeam = this.calculateDeltaRating(
+            percentageByTeam,
+            percentageBySelf,
+        );
+        const deltaPercentageByOther = this.calculateDeltaRating(
+            percentageByOther,
+            percentageBySelf,
+        );
+ 
+        return {
+            averageBySelfAssessment: this.toRoundedNumber(averageBySelf),
+            averageByTeam: this.toRoundedNumber(averageByTeam),
+            averageByOther: this.toRoundedNumber(averageByOther),
+            percentageBySelfAssessment: this.toRoundedNumber(percentageBySelf),
+            percentageByTeam: this.toRoundedNumber(percentageByTeam),
+            percentageByOther: this.toRoundedNumber(percentageByOther),
+            deltaPercentageByTeam: this.toRoundedNumber(deltaPercentageByTeam),
+            deltaPercentageByOther: this.toRoundedNumber(
+                deltaPercentageByOther,
+            ),
+        };
+    }
+ 
+    /**
+     * Calculates the metrics insights for entities (questions or competences).
+     * @param entities The entities to calculate the metrics insights for.
+     * @param type The type of entities (question or competence).
+     * @param reportId The ID of the report.
+     * @returns The metrics insights.
+     */
+    private calculateEntityInsights(
+        entities: ReportAnalyticsDomain[],
+        type: EntityType,
+        reportId: number,
+    ): ReportInsightDomain[] {
+        const averages: EntityAverageInsightsDto[] = [...entities]
+            .sort((a, b) => {
+                const nameA =
+                    type === EntityType.QUESTION
+                        ? (a.questionTitle ?? '')
+                        : (a.competenceTitle ?? '');
+                const nameB =
+                    type === EntityType.QUESTION
+                        ? (b.questionTitle ?? '')
+                        : (b.competenceTitle ?? '');
+                return nameA.localeCompare(nameB);
+            })
+            .map((entity, index) => {
+                return {
+                    entityId:
+                        type === EntityType.QUESTION
+                            ? entity.questionId
+                            : entity.competenceId,
+                    entityType: type,
+                    entityTitle:
+                        (type === EntityType.QUESTION
+                            ? entity.questionTitle
+                            : entity.competenceTitle) ?? '',
+                    num: index + 1,
+                    averageScore: this.getValidAverage([
+                        entity.averageByTeam,
+                        entity.averageByOther,
+                    ]),
+                    averageRating: this.getValidAverage([
+                        entity.percentageByTeam,
+                        entity.percentageByOther,
+                    ]),
+                    averageDelta: this.getValidAverage([
+                        entity.deltaPercentageByTeam,
+                        entity.deltaPercentageByOther,
+                    ]),
+                };
+            });
+ 
+        const validRatings = averages
+            .map((e) => e.averageRating)
+            .filter((v): v is number => v !== undefined && v !== null);
+        const validDeltas = averages
+            .map((e) => e.averageDelta)
+            .filter((v): v is number => v !== undefined && v !== null);
+ 
+        const maxRating =
+            validRatings.length > 0 ? Math.max(...validRatings) : null;
+        const minRating =
+            validRatings.length > 0 ? Math.min(...validRatings) : null;
+        const maxDelta =
+            validDeltas.length > 0 ? Math.max(...validDeltas) : null;
+        const minDelta =
+            validDeltas.length > 0 ? Math.min(...validDeltas) : null;
+ 
+        const insightSummaries: EntityInsightSummaryDto = {
+            highestRating:
+                maxRating !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageRating ?? 0).equals(maxRating),
+                      )
+                    : undefined,
+            lowestRating:
+                minRating !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageRating ?? 0).equals(minRating),
+                      )
+                    : undefined,
+            highestDelta:
+                maxDelta !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageDelta ?? 0).equals(maxDelta),
+                      )
+                    : undefined,
+            lowestDelta:
+                minDelta !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageDelta ?? 0).equals(minDelta),
+                      )
+                    : undefined,
+        };
+ 
+        const insights: ReportInsightDomain[] = [];
+ 
+        insights.push(
+            ReportInsightDomain.create({
+                reportId: reportId!,
+                insightType: InsightType.HIGHEST_RATING,
+                entityType: type,
+                questionId:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.highestRating?.entityId ?? null)
+                        : null,
+                questionTitle:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.highestRating?.entityTitle ?? null)
+                        : null,
+                competenceId:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.highestRating?.entityId ?? null)
+                        : null,
+                competenceTitle:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.highestRating?.entityTitle ?? null)
+                        : null,
+                averageScore: insightSummaries.highestRating?.averageScore,
+                averageRating: insightSummaries.highestRating?.averageRating,
+                averageDelta: insightSummaries.highestRating?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            ReportInsightDomain.create({
+                reportId: reportId!,
+                insightType: InsightType.LOWEST_RATING,
+                entityType: type,
+                questionId:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.lowestRating?.entityId ?? null)
+                        : null,
+                questionTitle:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.lowestRating?.entityTitle ?? null)
+                        : null,
+                competenceId:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.lowestRating?.entityId ?? null)
+                        : null,
+                competenceTitle:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.lowestRating?.entityTitle ?? null)
+                        : null,
+                averageScore: insightSummaries.lowestRating?.averageScore,
+                averageRating: insightSummaries.lowestRating?.averageRating,
+                averageDelta: insightSummaries.lowestRating?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            ReportInsightDomain.create({
+                reportId: reportId!,
+                insightType: InsightType.HIGHEST_DELTA,
+                entityType: type,
+                questionId:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.highestDelta?.entityId ?? null)
+                        : null,
+                questionTitle:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.highestDelta?.entityTitle ?? null)
+                        : null,
+                competenceId:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.highestDelta?.entityId ?? null)
+                        : null,
+                competenceTitle:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.highestDelta?.entityTitle ?? null)
+                        : null,
+                averageScore: insightSummaries.highestDelta?.averageScore,
+                averageRating: insightSummaries.highestDelta?.averageRating,
+                averageDelta: insightSummaries.highestDelta?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            ReportInsightDomain.create({
+                reportId: reportId!,
+                insightType: InsightType.LOWEST_DELTA,
+                entityType: type,
+                questionId:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.lowestDelta?.entityId ?? null)
+                        : null,
+                questionTitle:
+                    type === EntityType.QUESTION
+                        ? (insightSummaries.lowestDelta?.entityTitle ?? null)
+                        : null,
+                competenceId:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.lowestDelta?.entityId ?? null)
+                        : null,
+                competenceTitle:
+                    type === EntityType.COMPETENCE
+                        ? (insightSummaries.lowestDelta?.entityTitle ?? null)
+                        : null,
+                averageScore: insightSummaries.lowestDelta?.averageScore,
+                averageRating: insightSummaries.lowestDelta?.averageRating,
+                averageDelta: insightSummaries.lowestDelta?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        return insights;
+    }
+ 
+    /**
+     * Calculates the average of an array of numbers.
+     * @param arr The array of numbers.
+     * @returns The average of the numbers.
+     */
+    private getValidAverage = (arr: (Decimal | null | undefined)[]) => {
+        const validValues = arr.filter(
+            (val): val is Decimal => val !== null && val !== undefined,
+        );
+        Iif (validValues.length === 0) return null;
+        return this.calculateAverageNumberForArray(validValues);
+    };
+ 
+    /**
+     * Calculates the average of an array of numbers.
+     * @param numbers The array of numbers.
+     * @returns The average of the numbers.
+     */
+    private calculateAverageNumberForArray(numbers: Decimal[]): Decimal {
+        let sum = Decimal(0);
+        for (const number of numbers) {
+            sum = sum.add(number);
+        }
+        return sum.div(numbers.length);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/startegic-report-insight.service.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/startegic-report-insight.service.ts.html new file mode 100644 index 00000000..9f8e9bb8 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/startegic-report-insight.service.ts.html @@ -0,0 +1,241 @@ + + + + + + Code coverage report for reporting/application/services/startegic-report-insight.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services startegic-report-insight.service.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 75% + Branches + 9/12 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +532x +  +  +2x +  +  +  +2x +  +  +2x +  +  +4x +4x +  +  +  +  +  +  +2x +  +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { StrategicReportInsightDomain } from '../../domain/startegic-report-insight.domain';
+import {
+    STRATEGIC_REPORT_INSIGHT_REPOSITORY,
+    StrategicReportInsightRepositoryPort,
+} from '../ports/strategic-report-insight.repository.port';
+import { StrategicReportingService } from './strategic-reports.service';
+ 
+@Injectable()
+export class StartegicReportInsightService {
+    constructor(
+        @Inject(STRATEGIC_REPORT_INSIGHT_REPOSITORY)
+        private readonly insights: StrategicReportInsightRepositoryPort,
+        private readonly reportingService: StrategicReportingService,
+    ) {}
+ 
+    async getByStrategicReportId(
+        strategicReportId: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportInsightDomain[]> {
+        await this.reportingService.getById(strategicReportId, actor);
+ 
+        const insights =
+            await this.insights.findByStrategicReportId(strategicReportId);
+ 
+        if (!insights) {
+            throw new NotFoundException(
+                `Report insights with strategic report id ${strategicReportId} was not found`,
+            );
+        }
+ 
+        return insights;
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportInsightDomain> {
+        const insight = await this.insights.findById(id);
+ 
+        if (!insight) {
+            throw new NotFoundException(
+                `Report insight with id ${id} was not found`,
+            );
+        }
+ 
+        await this.reportingService.getById(insight.strategicReportId, actor);
+ 
+        return insight;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/strategic-report-analytics.service.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/strategic-report-analytics.service.ts.html new file mode 100644 index 00000000..c5cc70ed --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/strategic-report-analytics.service.ts.html @@ -0,0 +1,256 @@ + + + + + + Code coverage report for reporting/application/services/strategic-report-analytics.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services strategic-report-analytics.service.ts

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 75% + Branches + 9/12 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +582x +  +  +2x +  +  +  +2x +  +  +2x +  +  +4x +4x +  +  +  +  +  +  +2x +  +  +2x +  +  +  +2x +1x +  +  +  +  +1x +  +  +  +  +  +  +2x +  +2x +1x +  +  +  +  +1x +  +  +  +  +1x +  +  + 
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import { StrategicReportAnalyticsDomain } from '../../domain/strategic-report-analytics.domain';
+import {
+    STRATEGIC_REPORT_ANALYTICS_REPOSITORY,
+    StrategicReportAnalyticsRepositoryPort,
+} from '../ports/strategic-report-analytics.repository.port';
+import { StrategicReportingService } from './strategic-reports.service';
+ 
+@Injectable()
+export class StrategicReportAnalyticsService {
+    constructor(
+        @Inject(STRATEGIC_REPORT_ANALYTICS_REPOSITORY)
+        private readonly strategicAnalytics: StrategicReportAnalyticsRepositoryPort,
+        private readonly strategicReportingService: StrategicReportingService,
+    ) {}
+ 
+    async getByStrategicReportId(
+        strategicReportId: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportAnalyticsDomain[]> {
+        await this.strategicReportingService.getById(strategicReportId, actor);
+ 
+        const strategicAnalytics =
+            await this.strategicAnalytics.findByStrategicReportId(
+                strategicReportId,
+            );
+ 
+        if (!strategicAnalytics) {
+            throw new NotFoundException(
+                `Strategic report analytics with report id ${strategicReportId} was not found`,
+            );
+        }
+ 
+        return strategicAnalytics;
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportAnalyticsDomain> {
+        const strategicAnalytics = await this.strategicAnalytics.findById(id);
+ 
+        if (!strategicAnalytics) {
+            throw new NotFoundException(
+                `Strategic report analytics with id ${id} was not found`,
+            );
+        }
+ 
+        await this.strategicReportingService.getById(
+            strategicAnalytics.strategicReportId,
+            actor,
+        );
+ 
+        return strategicAnalytics;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/strategic-reports.service.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/strategic-reports.service.ts.html new file mode 100644 index 00000000..dd55f10f --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/strategic-reports.service.ts.html @@ -0,0 +1,2953 @@ + + + + + + Code coverage report for reporting/application/services/strategic-reports.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services strategic-reports.service.ts

+
+ +
+ 95.23% + Statements + 280/294 +
+ + +
+ 70.25% + Branches + 163/232 +
+ + +
+ 88.7% + Functions + 55/62 +
+ + +
+ 95.74% + Lines + 270/282 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +9575x +  +  +  +  +  +  +  +  +  +  +5x +  +  +  +  +  +  +5x +5x +  +  +  +5x +  +  +  +5x +  +  +  +  +  +5x +  +  +  +  +5x +  +  +  +  +5x +  +  +  +5x +  +  +  +  +  +5x +  +  +  +5x +  +  +  +5x +  +  +  +  +  +  +  +5x +5x +5x +5x +  +  +  +5x +  +  +  +5x +  +  +  +5x +  +  +  +5x +  +  +  +  +  +5x +16x +  +  +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +16x +  +  +  +  +  +  +3x +2x +  +  +  +  +  +  +  +3x +  +  +2x +  +  +2x +1x +  +  +  +  +  +  +  +  +  +5x +  +5x +1x +  +  +  +  +4x +  +2x +  +  +  +  +  +  +  +  +  +  +  +5x +  +  +4x +  +  +4x +  +3x +  +3x +1x +  +  +  +2x +2x +  +  +2x +  +1x +  +  +  +  +  +  +  +  +2x +2x +1x +  +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +  +6x +  +6x +6x +1x +  +  +5x +5x +1x +1x +  +  +4x +4x +1x +  +  +  +  +3x +3x +  +  +  +  +  +  +3x +  +3x +3x +3x +2x +2x +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +3x +3x +3x +3x +  +  +  +  +3x +3x +3x +3x +3x +  +3x +2x +2x +2x +2x +  +2x +2x +2x +  +2x +1x +  +  +2x +1x +  +  +  +2x +  +  +  +2x +2x +  +2x +2x +  +  +  +2x +2x +2x +2x +2x +  +  +  +  +2x +1x +1x +1x +1x +  +  +  +  +2x +  +  +  +2x +2x +  +2x +2x +  +  +  +  +2x +  +  +  +2x +3x +  +  +  +3x +  +  +  +  +  +  +2x +  +1x +  +  +  +2x +2x +2x +  +2x +2x +  +  +  +  +2x +1x +1x +1x +1x +  +  +1x +1x +1x +  +  +  +2x +1x +  +1x +1x +1x +  +  +  +  +2x +2x +  +  +3x +  +3x +  +  +  +  +  +  +  +  +  +3x +3x +  +  +  +  +3x +  +  +  +  +3x +2x +  +3x +2x +1x +3x +3x +9x +  +3x +1x +  +3x +3x +  +  +3x +  +  +3x +  +  +  +3x +3x +  +3x +3x +  +3x +3x +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +3x +  +  +  +3x +  +  +  +3x +  +  +  +  +3x +  +  +  +3x +  +  +  +3x +  +  +3x +1x +  +  +  +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +12x +6x +6x +12x +6x +6x +  +  +6x +  +  +  +  +  +  +  +  +  +  +26x +  +26x +26x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +3x +1x +1x +1x +1x +1x +  +  +  +  +1x +1x +  +1x +1x +  +1x +1x +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +1x +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +3x +1x +  +3x +1x +  +3x +1x +  +  +3x +  +  +  +3x +  +  +  +3x +  +  +  +  +3x +  +  +  +3x +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +12x +6x +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +8x +4x +  +  +4x +  +  +  +  +  +  +  +  +24x +8x +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +1x +1x +3x +1x +1x +  +  +3x +  +3x +  +3x +  +3x +  +3x +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +3x +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +16x +3x +6x +  +3x +3x +  +  +  +  +  +  +  +  +3x +3x +6x +  +3x +  +  + 
import {
+    EntityAverageInsightsDto,
+    EntityInsightSummaryDto,
+    EntitySummaryTotals,
+    EntityType,
+    IdentityRole,
+    InsightType,
+    REPORT_ANALYTICS_CONSTRAINTS,
+    RespondentCategory,
+    StrategicReportSearchQuery,
+} from '@intra/shared-kernel';
+import {
+    ForbiddenException,
+    Inject,
+    Injectable,
+    Logger,
+    NotFoundException,
+} from '@nestjs/common';
+import Decimal from 'decimal.js';
+import {
+    CYCLE_REPOSITORY,
+    CycleRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/cycle.repository.port';
+import {
+    QUESTION_REPOSITORY,
+    QuestionRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/question.repository.port';
+import {
+    REVIEWER_REPOSITORY,
+    ReviewerRepositoryPort,
+} from 'src/contexts/feedback360/application/ports/reviewer.repository.port';
+import { QuestionDomain } from 'src/contexts/feedback360/domain/question.domain';
+import { ReviewerDomain } from 'src/contexts/feedback360/domain/reviewer.domain';
+import {
+    IDENTITY_USER_REPOSITORY,
+    UserRepositoryPort,
+} from 'src/contexts/identity/application/ports/user.repository.port';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import {
+    COMPETENCE_REPOSITORY,
+    CompetenceRepositoryPort,
+} from 'src/contexts/library/application/ports/competence.repository.port';
+import { CompetenceDomain } from 'src/contexts/library/domain/competence.domain';
+import {
+    ORGANISATION_POSITION_REPOSITORY,
+    PositionRepositoryPort,
+} from 'src/contexts/organisation/application/ports/position.repository.port';
+import {
+    ORGANISATION_TEAM_REPOSITORY,
+    TeamRepositoryPort,
+} from 'src/contexts/organisation/application/ports/team.repository.port';
+import { PositionDomain } from 'src/contexts/organisation/domain/position.domain';
+import { TeamDomain } from 'src/contexts/organisation/domain/team.domain';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../../../feedback360/application/ports/answer.repository.port';
+import {
+    RESPONDENT_REPOSITORY,
+    RespondentRepositoryPort,
+} from '../../../feedback360/application/ports/respondent.repository.port';
+import {
+    REVIEW_REPOSITORY,
+    ReviewRepositoryPort,
+} from '../../../feedback360/application/ports/review.repository.port';
+import { AnswerDomain } from '../../../feedback360/domain/answer.domain';
+import { RespondentDomain } from '../../../feedback360/domain/respondent.domain';
+import { ReviewDomain } from '../../../feedback360/domain/review.domain';
+import { ReportAnalyticsDomain } from '../../domain/report-analytics.domain';
+import { StrategicReportInsightDomain } from '../../domain/startegic-report-insight.domain';
+import { StrategicReportAnalyticsDomain } from '../../domain/strategic-report-analytics.domain';
+import { StrategicReportDomain } from '../../domain/strategic-report.domain';
+import {
+    REPORT_ANALYTICS_REPOSITORY,
+    ReportAnalyticsRepositoryPort,
+} from '../ports/report-analytics.repository.port';
+import {
+    REPORT_REPOSITORY,
+    ReportRepositoryPort,
+} from '../ports/report.repository.port';
+import {
+    STRATEGIC_REPORT_ANALYTICS_REPOSITORY,
+    StrategicReportAnalyticsRepositoryPort,
+} from '../ports/strategic-report-analytics.repository.port';
+import {
+    STRATEGIC_REPORT_INSIGHT_REPOSITORY,
+    StrategicReportInsightRepositoryPort,
+} from '../ports/strategic-report-insight.repository.port';
+import {
+    STRATEGIC_REPORT_REPOSITORY,
+    StrategicReportRepositoryPort,
+} from '../ports/strategic-report.repository.port';
+ 
+@Injectable()
+export class StrategicReportingService {
+    private readonly logger = new Logger(StrategicReportingService.name);
+ 
+    constructor(
+        @Inject(STRATEGIC_REPORT_REPOSITORY)
+        private readonly strategicReports: StrategicReportRepositoryPort,
+        @Inject(STRATEGIC_REPORT_ANALYTICS_REPOSITORY)
+        private readonly strategicAnalyticsRepo: StrategicReportAnalyticsRepositoryPort,
+        @Inject(RESPONDENT_REPOSITORY)
+        private readonly respondents: RespondentRepositoryPort,
+        @Inject(REVIEWER_REPOSITORY)
+        private readonly reviewers: ReviewerRepositoryPort,
+        @Inject(ANSWER_REPOSITORY)
+        private readonly answers: AnswerRepositoryPort,
+        @Inject(REVIEW_REPOSITORY)
+        private readonly reviews: ReviewRepositoryPort,
+        @Inject(CYCLE_REPOSITORY)
+        private readonly cyclesRepo: CycleRepositoryPort,
+        @Inject(REPORT_REPOSITORY)
+        private readonly reports: ReportRepositoryPort,
+        @Inject(IDENTITY_USER_REPOSITORY)
+        private readonly users: UserRepositoryPort,
+        @Inject(ORGANISATION_TEAM_REPOSITORY)
+        private readonly teams: TeamRepositoryPort,
+        @Inject(ORGANISATION_POSITION_REPOSITORY)
+        private readonly positions: PositionRepositoryPort,
+        @Inject(COMPETENCE_REPOSITORY)
+        private readonly competences: CompetenceRepositoryPort,
+        @Inject(QUESTION_REPOSITORY)
+        private readonly questions: QuestionRepositoryPort,
+        @Inject(REPORT_ANALYTICS_REPOSITORY)
+        private readonly reportAnalyticsRepo: ReportAnalyticsRepositoryPort,
+        @Inject(STRATEGIC_REPORT_INSIGHT_REPOSITORY)
+        private readonly insightsRepo: StrategicReportInsightRepositoryPort,
+    ) {}
+ 
+    async search(
+        query: StrategicReportSearchQuery,
+        actor?: UserDomain,
+    ): Promise<StrategicReportDomain[]> {
+        await this.checkAccessToAllStrategicReports(actor);
+        return await this.strategicReports.search(query);
+    }
+ 
+    /**
+     * Checks if the actor has access to all strategic reports (HR or Admin only).
+     * @param actor The actor to check access for.
+     */
+    async checkAccessToAllStrategicReports(actor?: UserDomain): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (!isAdminOrHr) {
+            throw new ForbiddenException(
+                'You do not have permission to view strategic reports',
+            );
+        }
+    }
+ 
+    async getById(
+        id: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportDomain> {
+        const report = await this.strategicReports.findById(id);
+ 
+        if (!report) {
+            throw new NotFoundException(
+                `Strategic report with id ${id} was not found`,
+            );
+        }
+ 
+        await this.checkAccessToStrategicReport(report, actor);
+ 
+        return report;
+    }
+ 
+    /**
+     * Checks if the actor has access to the strategic report.
+     * @param report The strategic report to check access for.
+     * @param actor The actor to check access for.
+     */
+    async checkAccessToStrategicReport(
+        report: StrategicReportDomain,
+        actor?: UserDomain,
+    ): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        const reviews = await this.reviews.search({ cycleId: report.cycleId });
+ 
+        if (!reviews || reviews.length === 0) {
+            throw new NotFoundException(`Reviews for strategic report ${report.id} was not found.
+                Unable to check access to strategic report`);
+        }
+ 
+        const isManagerOfReview = reviews.some(
+            (review) => review.managerId === actor.id,
+        );
+ 
+        if (isManagerOfReview) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view this strategic report',
+        );
+    }
+ 
+    async getByCycleId(
+        cycleId: number,
+        actor?: UserDomain,
+    ): Promise<StrategicReportDomain> {
+        const report = await this.strategicReports.findByCycleId(cycleId);
+        if (!report) {
+            throw new NotFoundException(
+                `Report for cycle ${cycleId} was not found`,
+            );
+        }
+ 
+        await this.checkAccessToStrategicReport(report, actor);
+ 
+        return report;
+    }
+ 
+    /**
+     * Generates a comprehensive strategic report for a feedback360 cycle.
+     * @param cycleId The ID of the cycle to generate a report for.
+     * @returns The generated report.
+     */
+    async generateStrategicReportForCycle(
+        cycleId: number,
+    ): Promise<StrategicReportDomain> {
+        this.logger.debug(`Starting report generation for cycle ${cycleId}`);
+ 
+        const cycle = await this.cyclesRepo.findById(cycleId);
+        if (!cycle) {
+            throw new NotFoundException(`Cycle ${cycleId} not found`);
+        }
+ 
+        const existing = await this.strategicReports.findByCycleId(cycleId);
+        if (existing) {
+            this.logger.debug(`Report already exists for cycle ${cycleId}`);
+            return existing;
+        }
+ 
+        const reports = await this.reports.search({ cycleId });
+        if (!reports || reports.length === 0) {
+            throw new NotFoundException(
+                `Reports for cycle ${cycleId} not found`,
+            );
+        }
+ 
+        const allReviews = new Set<ReviewDomain>();
+        const reviewIds = new Set<number>();
+ 
+        const turnouts: {
+            reportId: number;
+            rateeTurnout: Decimal;
+            teamTurnout: Decimal;
+            otherTurnout: Decimal;
+        }[] = [];
+ 
+        for (const report of reports) {
+            const review = await this.reviews.findById(report.reviewId);
+            if (review) {
+                allReviews.add(review);
+                reviewIds.add(review.id!);
+            }
+ 
+            turnouts.push({
+                reportId: report.id!,
+                rateeTurnout: new Decimal(0),
+                teamTurnout: report.turnoutPctOfTeam
+                    ? new Decimal(report.turnoutPctOfTeam)
+                    : new Decimal(0),
+                otherTurnout: report.turnoutPctOfOther
+                    ? new Decimal(report.turnoutPctOfOther)
+                    : new Decimal(0),
+            });
+        }
+ 
+        const allRatees = new Set<UserDomain>();
+        const allRespondents = new Set<RespondentDomain>();
+        const allAnswers = new Set<AnswerDomain>();
+        const actualAnswersByCategory: Record<string, Decimal> = {
+            [RespondentCategory.SELF_ASSESSMENT]: new Decimal(0),
+            [RespondentCategory.TEAM]: new Decimal(0),
+            [RespondentCategory.OTHER]: new Decimal(0),
+        };
+        const allReviewers = new Set<ReviewerDomain>();
+        const allTeams = new Map<number, TeamDomain>();
+        const allPositions = new Map<number, PositionDomain>();
+        const allCompetences = new Map<number, CompetenceDomain>();
+        const allQuestions = new Map<number, QuestionDomain>();
+ 
+        for (const review of allReviews) {
+            const teamIds = new Set<number>();
+            const positionIds = new Set<number>();
+            const competenceIds = new Set<number>();
+            const questionIds = new Set<number>();
+ 
+            const ratee = await this.users.findById(review.rateeId);
+            Eif (ratee) {
+                allRatees.add(ratee);
+ 
+                if (ratee.teamId) {
+                    teamIds.add(ratee.teamId);
+                }
+ 
+                if (ratee.positionId) {
+                    positionIds.add(ratee.positionId);
+                }
+            }
+ 
+            const respondents = await this.respondents.listByReview(
+                review.id!,
+                {},
+            );
+            respondents.forEach((respondent) => {
+                allRespondents.add(respondent);
+ 
+                Eif (respondent.teamId) {
+                    teamIds.add(respondent.teamId);
+                }
+            });
+ 
+            for (const teamId of teamIds) {
+                Eif (!allTeams.has(teamId)) {
+                    const team = await this.teams.findById(teamId);
+                    Eif (team) {
+                        allTeams.set(teamId, team);
+                    }
+                }
+            }
+ 
+            for (const positionId of positionIds) {
+                Eif (!allPositions.has(positionId)) {
+                    const position = await this.positions.findById(positionId);
+                    Eif (position) {
+                        allPositions.set(positionId, position);
+                    }
+                }
+            }
+ 
+            const answers = await this.answers.list({
+                reviewId: review.id!,
+            });
+ 
+            answers.forEach((answer) => {
+                allAnswers.add(answer);
+ 
+                Eif (answer.questionId) {
+                    questionIds.add(answer.questionId);
+                }
+            });
+ 
+            const answersCounts =
+                await this.answers.getAnswersCountByRespondentCategories(
+                    review.id!,
+                );
+ 
+            answersCounts.forEach((stat) => {
+                Eif (
+                    actualAnswersByCategory[stat.respondentCategory] !==
+                    undefined
+                ) {
+                    actualAnswersByCategory[stat.respondentCategory] =
+                        actualAnswersByCategory[stat.respondentCategory].plus(
+                            stat.answers,
+                        );
+                }
+            });
+ 
+            const selfAssessmentStat = answersCounts.find(
+                (stat) =>
+                    stat.respondentCategory ===
+                    RespondentCategory.SELF_ASSESSMENT,
+            );
+            const hasSelfAssessment =
+                selfAssessmentStat && selfAssessmentStat.answers > 0;
+            const currentTurnout = turnouts.find(
+                (t) => t.reportId === review.reportId,
+            );
+            Eif (currentTurnout) {
+                currentTurnout.rateeTurnout = hasSelfAssessment
+                    ? new Decimal(100)
+                    : new Decimal(0);
+            }
+ 
+            for (const questionId of questionIds) {
+                Eif (!allQuestions.has(questionId)) {
+                    const question = await this.questions.findById(questionId);
+                    Eif (question) {
+                        allQuestions.set(questionId, question);
+                    }
+                }
+                const q = allQuestions.get(questionId);
+                Eif (q && q.competenceId) {
+                    competenceIds.add(q.competenceId);
+                }
+            }
+ 
+            for (const competenceId of competenceIds) {
+                Eif (!allCompetences.has(competenceId)) {
+                    const competence =
+                        await this.competences.findById(competenceId);
+                    Eif (competence) {
+                        allCompetences.set(competenceId, competence);
+                    }
+                }
+            }
+ 
+            const reviewers = await this.reviewers.listByReview(review.id!, {});
+            reviewers.forEach((reviewer) => allReviewers.add(reviewer));
+        }
+ 
+        const allReportAnalytics = await Promise.all(
+            reports.map(async (report) => {
+                return {
+                    reportId: report.id!,
+                    reportAnalytics:
+                        await this.reportAnalyticsRepo.findByReportId(
+                            report.id!,
+                        ),
+                };
+            }),
+        );
+ 
+        const maxScore = new Decimal(REPORT_ANALYTICS_CONSTRAINTS.SCORE.MAX);
+        const competenceAnalytics = this.buildStrategicAnalyticPayload(
+            allReportAnalytics,
+            maxScore,
+            Array.from(allCompetences.values()),
+        );
+        const competenceTotals = this.calculateCompetenceSummaryTotals(
+            competenceAnalytics,
+            maxScore,
+        );
+ 
+        const allRateeIds = [
+            ...new Set<number>([...allRatees].map((r) => r.id!)),
+        ].sort((a, b) => a - b);
+        const allRespondentIds = [
+            ...new Set<number>([...allRespondents].map((r) => r.respondentId!)),
+        ].sort((a, b) => a - b);
+        let allAnswerCount = new Decimal(0);
+        Object.values(actualAnswersByCategory).forEach((count) => {
+            allAnswerCount = allAnswerCount.plus(count);
+        });
+        const allReviewerIds = [
+            ...new Set<number>([...allReviewers].map((r) => r.reviewerId!)),
+        ].sort((a, b) => a - b);
+        const allTeamIds = Array.from(allTeams.keys()).sort((a, b) => a - b);
+        const allPositionIds = Array.from(allPositions.keys()).sort(
+            (a, b) => a - b,
+        );
+        const allCompetenceIds = Array.from(allCompetences.keys()).sort(
+            (a, b) => a - b,
+        );
+        const allQuestionIds = Array.from(allQuestions.keys()).sort(
+            (a, b) => a - b,
+        );
+ 
+        const rateeTurnout = turnouts
+            .reduce((acc, t) => acc.plus(t.rateeTurnout), new Decimal(0))
+            .dividedBy(turnouts.length);
+        const teamTurnout = turnouts
+            .reduce((acc, t) => acc.plus(t.teamTurnout), new Decimal(0))
+            .dividedBy(turnouts.length);
+        const otherTurnout = turnouts
+            .reduce((acc, t) => acc.plus(t.otherTurnout), new Decimal(0))
+            .dividedBy(turnouts.length);
+ 
+        const report = StrategicReportDomain.create({
+            cycleId: cycleId,
+            cycleTitle: cycle.title,
+            rateeCount: allRateeIds.length,
+            rateeIds: allRateeIds,
+            respondentCount: allRespondentIds.length,
+            respondentIds: allRespondentIds,
+            answerCount: allAnswerCount.toNumber(),
+            reviewerCount: allReviewerIds.length,
+            reviewerIds: allReviewerIds,
+            teamCount: allTeamIds.length,
+            teamIds: allTeamIds,
+            positionCount: allPositionIds.length,
+            positionIds: allPositionIds,
+            competenceCount: allCompetenceIds.length,
+            competenceIds: allCompetenceIds,
+            questionCount: allQuestionIds.length,
+            questionIds: allQuestionIds,
+            turnoutAvgPctOfRatees: rateeTurnout,
+            turnoutAvgPctOfTeams: teamTurnout,
+            turnoutAvgPctOfOthers: otherTurnout,
+            competenceGeneralAvgSelf: competenceTotals.averageBySelfAssessment,
+            competenceGeneralAvgTeam: competenceTotals.averageByTeam,
+            competenceGeneralAvgOther: competenceTotals.averageByOther,
+            competenceGeneralPctSelf:
+                competenceTotals.percentageBySelfAssessment,
+            competenceGeneralPctTeam: competenceTotals.percentageByTeam,
+            competenceGeneralPctOther: competenceTotals.percentageByOther,
+            competenceGeneralDeltaTeam: competenceTotals.deltaPercentageByTeam,
+            competenceGeneralDeltaOther:
+                competenceTotals.deltaPercentageByOther,
+            analytics: [],
+        });
+ 
+        const createdReport = await this.strategicReports.create(report);
+ 
+        await this.strategicAnalyticsRepo.createMany(createdReport.id!, [
+            ...competenceAnalytics,
+        ]);
+ 
+        this.logger.debug(
+            `Generated ${competenceAnalytics.length} strategic report analytics for report ${createdReport.id}`,
+        );
+ 
+        const competenceInsights = await this.calculateEntityInsights(
+            competenceAnalytics,
+            createdReport.id!,
+        );
+ 
+        await this.insightsRepo.createMany(createdReport.id!, [
+            ...competenceInsights,
+        ]);
+ 
+        this.logger.debug(
+            `Generated ${competenceInsights.length} competence insights`,
+        );
+ 
+        const fullReport = await this.strategicReports.findById(
+            createdReport.id!,
+        );
+        if (!fullReport) {
+            throw new NotFoundException(
+                `Strategic report ${createdReport.id} could not be loaded after creation`,
+            );
+        }
+ 
+        this.logger.debug(
+            `Successfully created strategic report ${fullReport.id} for cycle ${cycleId}`,
+        );
+ 
+        return fullReport;
+    }
+ 
+    /**
+     * Calculates average of decimals, returns null if array is empty.
+     * Formula: sum / count
+     * @param numbers The array of numbers to calculate the average of.
+     * @returns The average of the numbers as a Decimal, or null if the array is empty.
+     */
+    private calculateAverage(
+        numbers: (Decimal | number | null | undefined)[],
+    ): Decimal | null {
+        const validNumbers = numbers
+            .filter((n): n is Decimal | number => n !== null && n !== undefined)
+            .map((n) => (n instanceof Decimal ? n : new Decimal(n)));
+        if (validNumbers.length === 0) return null;
+        const sum = validNumbers.reduce(
+            (acc, value) => acc.plus(value),
+            new Decimal(0),
+        );
+        return sum.dividedBy(validNumbers.length);
+    }
+ 
+    /**
+     * Normalizes decimal scale to 4 digits (or null)
+     * @param value The value to normalize.
+     * @returns The normalized value.
+     */
+    private roundDecimal(
+        value: Decimal | number | null | undefined,
+    ): Decimal | null {
+        Iif (value === null || value === undefined) return null;
+        const decimalValue =
+            value instanceof Decimal ? value : new Decimal(value);
+        return decimalValue.toDecimalPlaces(4);
+    }
+ 
+    /**
+     * Calculates the turnout percentage of respondents.
+     * Formula: (actual answers / assigned respondents) * 100
+     * @param respondentCount The count of all assigned respondents.
+     * @param actualAnswers The count of actual answers for the category.
+     * @returns The turnout percentage as a Decimal.
+     */
+    private calculateTurnout(
+        respondentCount: Decimal,
+        actualAnswers: Decimal,
+    ): Decimal {
+        if (actualAnswers?.equals(0)) {
+            return new Decimal(0);
+        }
+ 
+        const turnout = actualAnswers.dividedBy(respondentCount).times(100);
+ 
+        return this.roundDecimal(turnout) ?? new Decimal(0);
+    }
+ 
+    /**
+     * Builds the strategic analytics payload for a report, calculating averages
+     * and percentages for competences from all reports.
+     * @param reportAnalytics The report analytics for each report.
+     * @param maxScore The maximum score for the competences.
+     * @param allCompetences The list of all rated competences.
+     * @returns An object containing competence analytics.
+     */
+    private buildStrategicAnalyticPayload(
+        reportAnalytics: {
+            reportId: number;
+            reportAnalytics: ReportAnalyticsDomain[];
+        }[],
+        maxScore: Decimal,
+        allCompetences: CompetenceDomain[],
+    ): StrategicReportAnalyticsDomain[] {
+        const competenceAnalytics: StrategicReportAnalyticsDomain[] = [];
+ 
+        for (const competence of allCompetences) {
+            const currentCompetenceScores: ReportAnalyticsDomain[] = [];
+            reportAnalytics.forEach((r) => {
+                r.reportAnalytics?.forEach((a) => {
+                    Eif (a.competenceId === competence.id) {
+                        currentCompetenceScores.push(a);
+                    }
+                });
+            });
+ 
+            const averageBySelf = this.calculateAverage(
+                currentCompetenceScores.map((a) => a.averageBySelfAssessment),
+            );
+            const averageByTeam = this.calculateAverage(
+                currentCompetenceScores.map((a) => a.averageByTeam),
+            );
+            const averageByOther = this.calculateAverage(
+                currentCompetenceScores.map((a) => a.averageByOther),
+            );
+ 
+            const percentageBySelf = this.calculatePercentage(
+                averageBySelf,
+                maxScore,
+            );
+            const percentageByTeam = this.calculatePercentage(
+                averageByTeam,
+                maxScore,
+            );
+            const percentageByOther = this.calculatePercentage(
+                averageByOther,
+                maxScore,
+            );
+ 
+            const deltaPercentageByTeam = this.calculateDelta(
+                percentageByTeam,
+                percentageBySelf,
+            );
+            const deltaPercentageByOther = this.calculateDelta(
+                percentageByOther,
+                percentageBySelf,
+            );
+ 
+            competenceAnalytics.push(
+                StrategicReportAnalyticsDomain.create({
+                    strategicReportId: 0,
+                    competenceId: competence.id!,
+                    competenceTitle: competence.title!,
+                    averageBySelfAssessment:
+                        this.roundDecimal(averageBySelf) ?? null,
+                    averageByTeam: this.roundDecimal(averageByTeam) ?? null,
+                    averageByOther: this.roundDecimal(averageByOther) ?? null,
+                    percentageBySelfAssessment:
+                        this.roundDecimal(percentageBySelf) ?? null,
+                    percentageByTeam:
+                        this.roundDecimal(percentageByTeam) ?? null,
+                    percentageByOther:
+                        this.roundDecimal(percentageByOther) ?? null,
+                    deltaPercentageByTeam:
+                        this.roundDecimal(deltaPercentageByTeam) ?? null,
+                    deltaPercentageByOther:
+                        this.roundDecimal(deltaPercentageByOther) ?? null,
+                }),
+            );
+        }
+ 
+        return competenceAnalytics;
+    }
+ 
+    /**
+     * Calculates the summary totals for all rated competences.
+     * @param strategicAnalytics The strategic analytics for the competences.
+     * @param maxScore The maximum score.
+     * @returns The summary competence totals.
+     */
+    private calculateCompetenceSummaryTotals(
+        strategicAnalytics: StrategicReportAnalyticsDomain[],
+        maxScore: Decimal,
+    ): EntitySummaryTotals {
+        const averageBySelf = this.calculateAverage(
+            strategicAnalytics.map((item) => item.averageBySelfAssessment),
+        );
+        const averageByTeam = this.calculateAverage(
+            strategicAnalytics.map((item) => item.averageByTeam),
+        );
+        const averageByOther = this.calculateAverage(
+            strategicAnalytics.map((item) => item.averageByOther),
+        );
+ 
+        const percentageBySelf = this.calculatePercentage(
+            averageBySelf,
+            maxScore,
+        );
+        const percentageByTeam = this.calculatePercentage(
+            averageByTeam,
+            maxScore,
+        );
+        const percentageByOther = this.calculatePercentage(
+            averageByOther,
+            maxScore,
+        );
+ 
+        const deltaPercentageByTeam = this.calculateDelta(
+            percentageByTeam,
+            percentageBySelf,
+        );
+        const deltaPercentageByOther = this.calculateDelta(
+            percentageByOther,
+            percentageBySelf,
+        );
+        return {
+            averageBySelfAssessment: this.toRoundedNumber(averageBySelf),
+            averageByTeam: this.toRoundedNumber(averageByTeam),
+            averageByOther: this.toRoundedNumber(averageByOther),
+            percentageBySelfAssessment: this.toRoundedNumber(percentageBySelf),
+            percentageByTeam: this.toRoundedNumber(percentageByTeam),
+            percentageByOther: this.toRoundedNumber(percentageByOther),
+            deltaPercentageByTeam: this.toRoundedNumber(deltaPercentageByTeam),
+            deltaPercentageByOther: this.toRoundedNumber(
+                deltaPercentageByOther,
+            ),
+        };
+    }
+ 
+    /**
+     * Calculates the percentage of a value relative to a maximum score.
+     * Formula: (value / maxScore) * 100
+     * @param value The value to calculate the percentage of.
+     * @param maxScore The maximum score.
+     * @returns The percentage.
+     */
+    private calculatePercentage(
+        value: Decimal | null,
+        maxScore: Decimal,
+    ): Decimal | null {
+        if (value === null || maxScore.isZero()) {
+            return null;
+        }
+        return this.roundDecimal(value.dividedBy(maxScore).times(100));
+    }
+ 
+    /**
+     * Calculates the difference between two values in percentage points.
+     * Formula: respondent rating - self rating.
+     * @param respondent The respondent rating.
+     * @param self The self rating.
+     * @returns The difference in percentage points.
+     */
+    private calculateDelta(
+        respondent: Decimal | null,
+        self: Decimal | null,
+    ): Decimal | null {
+        if (respondent === null || self === null) {
+            return null;
+        }
+ 
+        return this.roundDecimal(respondent.minus(self));
+    }
+ 
+    /**
+     * Rounds a Decimal value to 4 decimal places.
+     * @param value The value to round.
+     * @returns The rounded value.
+     */
+    private toRoundedNumber(value: Decimal | null): number | null {
+        if (value === null) return null;
+        return Number(this.roundDecimal(value)?.toFixed(4));
+    }
+ 
+    /**
+     * Calculates the metrics insights for entities (questions or competences).
+     * @param entities The entities to calculate the metrics insights for.
+     * @param type The type of entities (question or competence).
+     * @param reportId The ID of the report.
+     * @returns The metrics insights.
+     */
+    private calculateEntityInsights(
+        entities: StrategicReportAnalyticsDomain[],
+        strategicReportId: number,
+    ): StrategicReportInsightDomain[] {
+        const averages: EntityAverageInsightsDto[] = [...entities]
+            .sort((a, b) => {
+                const nameA = a.competenceTitle ?? '';
+                const nameB = b.competenceTitle ?? '';
+                return nameA.localeCompare(nameB);
+            })
+            .map((entity, index) => {
+                return {
+                    entityId: entity.competenceId,
+                    entityType: EntityType.COMPETENCE,
+                    entityTitle: entity.competenceTitle,
+                    questionId: null,
+                    questionTitle: null,
+                    num: index + 1,
+                    averageScore: this.getValidAverage([
+                        entity.averageByTeam,
+                        entity.averageByOther,
+                    ]),
+                    averageRating: this.getValidAverage([
+                        entity.percentageByTeam,
+                        entity.percentageByOther,
+                    ]),
+                    averageDelta: this.getValidAverage([
+                        entity.deltaPercentageByTeam,
+                        entity.deltaPercentageByOther,
+                    ]),
+                };
+            });
+ 
+        const validRatings = averages
+            .map((e) => e.averageRating)
+            .filter((v): v is number => v !== undefined && v !== null);
+        const validDeltas = averages
+            .map((e) => e.averageDelta)
+            .filter((v): v is number => v !== undefined && v !== null);
+ 
+        const maxRating =
+            validRatings.length > 0 ? Math.max(...validRatings) : null;
+        const minRating =
+            validRatings.length > 0 ? Math.min(...validRatings) : null;
+        const maxDelta =
+            validDeltas.length > 0 ? Math.max(...validDeltas) : null;
+        const minDelta =
+            validDeltas.length > 0 ? Math.min(...validDeltas) : null;
+ 
+        const insightSummaries: EntityInsightSummaryDto = {
+            highestRating:
+                maxRating !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageRating ?? 0).equals(maxRating),
+                      )
+                    : undefined,
+            lowestRating:
+                minRating !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageRating ?? 0).equals(minRating),
+                      )
+                    : undefined,
+            highestDelta:
+                maxDelta !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageDelta ?? 0).equals(maxDelta),
+                      )
+                    : undefined,
+            lowestDelta:
+                minDelta !== null
+                    ? averages.find((e) =>
+                          new Decimal(e.averageDelta ?? 0).equals(minDelta),
+                      )
+                    : undefined,
+        };
+ 
+        const insights: StrategicReportInsightDomain[] = [];
+ 
+        insights.push(
+            StrategicReportInsightDomain.create({
+                strategicReportId: strategicReportId!,
+                insightType: InsightType.HIGHEST_RATING,
+                competenceId: insightSummaries.highestRating?.entityId ?? -1,
+                competenceTitle:
+                    insightSummaries.highestRating?.entityTitle ?? '',
+                averageScore: insightSummaries.highestRating?.averageScore,
+                averageRating: insightSummaries.highestRating?.averageRating,
+                averageDelta: insightSummaries.highestRating?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            StrategicReportInsightDomain.create({
+                strategicReportId: strategicReportId!,
+                insightType: InsightType.LOWEST_RATING,
+                competenceId: insightSummaries.lowestRating?.entityId ?? -1,
+                competenceTitle:
+                    insightSummaries.lowestRating?.entityTitle ?? '',
+                averageScore: insightSummaries.lowestRating?.averageScore,
+                averageRating: insightSummaries.lowestRating?.averageRating,
+                averageDelta: insightSummaries.lowestRating?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            StrategicReportInsightDomain.create({
+                strategicReportId: strategicReportId!,
+                insightType: InsightType.HIGHEST_DELTA,
+                competenceId: insightSummaries.highestDelta?.entityId ?? -1,
+                competenceTitle:
+                    insightSummaries.highestDelta?.entityTitle ?? '',
+                averageScore: insightSummaries.highestDelta?.averageScore,
+                averageRating: insightSummaries.highestDelta?.averageRating,
+                averageDelta: insightSummaries.highestDelta?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        insights.push(
+            StrategicReportInsightDomain.create({
+                strategicReportId: strategicReportId!,
+                insightType: InsightType.LOWEST_DELTA,
+                competenceId: insightSummaries.lowestDelta?.entityId ?? -1,
+                competenceTitle:
+                    insightSummaries.lowestDelta?.entityTitle ?? '',
+                averageScore: insightSummaries.lowestDelta?.averageScore,
+                averageRating: insightSummaries.lowestDelta?.averageRating,
+                averageDelta: insightSummaries.lowestDelta?.averageDelta,
+                createdAt: undefined,
+            }),
+        );
+ 
+        return insights;
+    }
+ 
+    /**
+     * Calculates the average of an array of numbers.
+     * @param arr The array of numbers.
+     * @returns The average of the numbers.
+     */
+    private getValidAverage = (arr: (Decimal | null | undefined)[]) => {
+        const validValues = arr.filter(
+            (val): val is Decimal => val !== null && val !== undefined,
+        );
+        Iif (validValues.length === 0) return null;
+        return this.calculateAverageNumberForArray(validValues);
+    };
+ 
+    /**
+     * Calculates the average of an array of numbers.
+     * @param numbers The array of numbers.
+     * @returns The average of the numbers.
+     */
+    private calculateAverageNumberForArray(numbers: Decimal[]): Decimal {
+        let sum = Decimal(0);
+        for (const number of numbers) {
+            sum = sum.add(number);
+        }
+        return sum.div(numbers.length);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/reporting/application/services/text-answer.service.ts.html b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/text-answer.service.ts.html new file mode 100644 index 00000000..0ebc635c --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/reporting/application/services/text-answer.service.ts.html @@ -0,0 +1,322 @@ + + + + + + Code coverage report for reporting/application/services/text-answer.service.ts + + + + + + + + + +
+
+

All files / reporting/application/services text-answer.service.ts

+
+ +
+ 100% + Statements + 24/24 +
+ + +
+ 77.77% + Branches + 14/18 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 20/20 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +802x +  +  +  +  +2x +  +2x +  +  +  +2x +  +  +  +  +  +  +2x +  +  +4x +  +4x +  +  +  +  +  +  +4x +  +3x +  +  +  +  +  +  +  +3x +3x +1x +  +  +3x +  +4x +  +  +2x +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +4x +  +  +3x +  +  +3x +  +1x +  +  +  +  + 
import {
+    AnswerType,
+    IdentityRole,
+    ReportTextAnswerDto,
+} from '@intra/shared-kernel';
+import { ForbiddenException, Inject, Injectable } from '@nestjs/common';
+import { UserDomain } from 'src/contexts/identity/domain/user.domain';
+import {
+    ANSWER_REPOSITORY,
+    AnswerRepositoryPort,
+} from '../../../feedback360/application/ports/answer.repository.port';
+import {
+    REVIEW_QUESTION_RELATION_REPOSITORY,
+    ReviewQuestionRelationRepositoryPort,
+} from '../../../feedback360/application/ports/review-question-relation.repository.port';
+import { ReviewQuestionRelationDomain } from '../../../feedback360/domain/review-question-relation.domain';
+ 
+@Injectable()
+export class TextAnswerService {
+    constructor(
+        @Inject(ANSWER_REPOSITORY)
+        private readonly answers: AnswerRepositoryPort,
+        @Inject(REVIEW_QUESTION_RELATION_REPOSITORY)
+        private readonly relations: ReviewQuestionRelationRepositoryPort,
+    ) {}
+ 
+    async listByReview(
+        reviewId: number,
+        actor?: UserDomain,
+    ): Promise<ReportTextAnswerDto[]> {
+        await this.checkAccessToTextAnswers(actor);
+ 
+        const [answers, relations] = await Promise.all([
+            this.answers.list({
+                reviewId,
+                answerType: AnswerType.TEXT_FIELD,
+            }),
+            this.relations.listByReview(reviewId, {}),
+        ]);
+ 
+        const relationMap = new Map<number, ReviewQuestionRelationDomain>();
+        relations.forEach((relation) => {
+            relationMap.set(relation.questionId, relation);
+        });
+ 
+        return answers
+            .filter(
+                (answer) => answer.textValue && answer.textValue.trim() !== '',
+            )
+            .map((answer) => {
+                const relation = relationMap.get(answer.questionId);
+                return {
+                    questionId: answer.questionId,
+                    questionTitle: relation?.questionTitle ?? null,
+                    respondentCategory: answer.respondentCategory,
+                    textValue: answer.textValue!,
+                };
+            });
+    }
+ 
+    /**
+     * Checks if the actor has access to the text answers
+     * as an admin or hr.
+     * @param actor The actor to check access for.
+     */
+    private async checkAccessToTextAnswers(actor?: UserDomain): Promise<void> {
+        if (!actor) return;
+ 
+        const isAdminOrHr =
+            actor?.roles?.includes(IdentityRole.ADMIN) ||
+            actor?.roles?.includes(IdentityRole.HR);
+ 
+        if (isAdminOrHr) return;
+ 
+        throw new ForbiddenException(
+            'You do not have permission to view text answers for this review',
+        );
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/apps/api/test/unit/coverage/lcov-report/sort-arrow-sprite.png b/apps/api/test/unit/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 00000000..6ed68316 Binary files /dev/null and b/apps/api/test/unit/coverage/lcov-report/sort-arrow-sprite.png differ diff --git a/apps/api/test/unit/coverage/lcov-report/sorter.js b/apps/api/test/unit/coverage/lcov-report/sorter.js new file mode 100644 index 00000000..4ed70ae5 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/apps/api/test/unit/coverage/lcov.info b/apps/api/test/unit/coverage/lcov.info new file mode 100644 index 00000000..ddd47404 --- /dev/null +++ b/apps/api/test/unit/coverage/lcov.info @@ -0,0 +1,4735 @@ +TN: +SF:src/contexts/feedback360/application/constants/cycle-stage-transitions.ts +FNF:0 +FNH:0 +DA:1,19 +DA:7,19 +DA:26,19 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/constants/response-status-transitions.ts +FNF:0 +FNH:0 +DA:1,8 +DA:7,8 +DA:29,8 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/constants/review-stage-transitions.ts +FNF:0 +FNH:0 +DA:1,8 +DA:7,8 +DA:51,8 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/events/cycle-stage-changed.event.ts +FN:4,(anonymous_0) +FNF:1 +FNH:1 +FNDA:15,(anonymous_0) +DA:3,19 +DA:5,15 +DA:6,15 +DA:7,15 +DA:8,15 +DA:9,15 +DA:10,15 +LF:7 +LH:7 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/events/cycle-stage-processed.event.ts +FN:5,(anonymous_0) +FN:13,(anonymous_1) +FNF:2 +FNH:2 +FNDA:9,(anonymous_0) +FNDA:6,(anonymous_1) +DA:4,3 +DA:6,9 +DA:7,9 +DA:8,9 +DA:9,9 +DA:10,9 +DA:16,6 +LF:7 +LH:7 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/events/respondent-status-changed.event.ts +FN:4,(anonymous_0) +FNF:1 +FNH:1 +FNDA:12,(anonymous_0) +DA:3,7 +DA:5,12 +DA:6,12 +DA:7,12 +DA:8,12 +DA:9,12 +DA:10,12 +DA:11,12 +LF:8 +LH:8 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/events/review-stage-changed.event.ts +FN:4,(anonymous_0) +FNF:1 +FNH:1 +FNDA:18,(anonymous_0) +DA:3,10 +DA:5,18 +DA:6,18 +DA:7,18 +DA:8,18 +DA:9,18 +DA:10,18 +LF:7 +LH:7 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/events/review-stage-processed.event.ts +FN:5,(anonymous_0) +FN:13,(anonymous_1) +FNF:2 +FNH:2 +FNDA:11,(anonymous_0) +FNDA:8,(anonymous_1) +DA:4,3 +DA:6,11 +DA:7,11 +DA:8,11 +DA:9,11 +DA:10,11 +DA:16,8 +LF:7 +LH:7 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/listeners/cycle-stage.listener.ts +FN:21,(anonymous_4) +FN:34,(anonymous_5) +FNF:2 +FNH:2 +FNDA:4,(anonymous_4) +FNDA:4,(anonymous_5) +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +DA:18,1 +DA:19,4 +DA:22,4 +DA:23,4 +DA:25,4 +DA:34,1 +DA:37,4 +DA:41,4 +DA:43,1 +DA:47,1 +DA:50,3 +DA:52,3 +DA:53,1 +DA:56,1 +DA:60,1 +DA:63,2 +DA:67,2 +DA:68,2 +DA:70,1 +DA:74,1 +DA:79,2 +LF:28 +LH:28 +BRDA:41,0,0,1 +BRDA:41,0,1,3 +BRDA:52,1,0,1 +BRDA:52,1,1,2 +BRDA:35,2,0,1 +BRDA:35,2,1,0 +BRDA:35,3,0,1 +BRDA:35,3,1,1 +BRDA:36,4,0,1 +BRDA:36,4,1,0 +BRDA:36,5,0,1 +BRDA:36,5,1,1 +BRDA:22,6,0,1 +BRDA:22,6,1,0 +BRDA:22,7,0,1 +BRDA:22,7,1,1 +BRDA:22,8,0,1 +BRDA:22,8,1,0 +BRDA:22,9,0,1 +BRDA:22,9,1,1 +BRDA:23,10,0,0 +BRDA:23,10,1,1 +BRDA:23,11,0,1 +BRDA:23,11,1,0 +BRF:24 +BRH:18 +end_of_record +TN: +SF:src/contexts/feedback360/application/listeners/respondent-status.listener.ts +FN:28,(anonymous_4) +FN:41,(anonymous_5) +FN:65,(anonymous_6) +FN:79,(anonymous_7) +FN:109,(anonymous_8) +FN:118,(anonymous_9) +FNF:6 +FNH:6 +FNDA:4,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:4,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:6,(anonymous_8) +FNDA:6,(anonymous_9) +DA:1,1 +DA:6,1 +DA:7,1 +DA:8,1 +DA:9,1 +DA:10,1 +DA:14,1 +DA:18,1 +DA:25,1 +DA:26,4 +DA:30,4 +DA:31,4 +DA:32,4 +DA:41,1 +DA:44,4 +DA:48,4 +DA:52,1 +DA:55,3 +DA:59,3 +DA:60,3 +DA:64,2 +DA:66,4 +DA:70,2 +DA:71,1 +DA:72,1 +DA:76,1 +DA:80,2 +DA:89,1 +DA:106,1 +DA:107,6 +DA:111,6 +DA:113,6 +DA:114,6 +DA:118,1 +DA:121,6 +DA:122,1 +DA:125,5 +DA:126,5 +DA:130,5 +DA:131,1 +DA:134,4 +DA:135,4 +DA:136,1 +DA:139,1 +DA:142,3 +DA:143,1 +DA:146,2 +DA:150,2 +DA:158,1 +LF:49 +LH:49 +BRDA:48,0,0,1 +BRDA:48,0,1,3 +BRDA:49,1,0,4 +BRDA:49,1,1,1 +BRDA:66,2,0,4 +BRDA:66,2,1,2 +BRDA:70,3,0,1 +BRDA:70,3,1,1 +BRDA:80,4,0,2 +BRDA:80,4,1,1 +BRDA:42,5,0,1 +BRDA:42,5,1,0 +BRDA:42,6,0,1 +BRDA:42,6,1,1 +BRDA:43,7,0,1 +BRDA:43,7,1,0 +BRDA:43,8,0,1 +BRDA:43,8,1,1 +BRDA:30,9,0,0 +BRDA:30,9,1,1 +BRDA:30,10,0,1 +BRDA:30,10,1,0 +BRDA:30,11,0,1 +BRDA:30,11,1,0 +BRDA:30,12,0,1 +BRDA:30,12,1,1 +BRDA:31,13,0,1 +BRDA:31,13,1,0 +BRDA:31,14,0,1 +BRDA:31,14,1,1 +BRDA:121,15,0,1 +BRDA:121,15,1,5 +BRDA:130,16,0,1 +BRDA:130,16,1,4 +BRDA:135,17,0,1 +BRDA:135,17,1,3 +BRDA:142,18,0,1 +BRDA:142,18,1,2 +BRDA:153,19,0,2 +BRDA:153,19,1,1 +BRDA:154,20,0,2 +BRDA:154,20,1,1 +BRDA:119,21,0,1 +BRDA:119,21,1,0 +BRDA:119,22,0,1 +BRDA:119,22,1,1 +BRDA:120,23,0,1 +BRDA:120,23,1,0 +BRDA:120,24,0,1 +BRDA:120,24,1,1 +BRDA:111,25,0,0 +BRDA:111,25,1,1 +BRDA:111,26,0,1 +BRDA:111,26,1,0 +BRDA:111,27,0,0 +BRDA:111,27,1,1 +BRDA:111,28,0,1 +BRDA:111,28,1,0 +BRDA:113,29,0,1 +BRDA:113,29,1,0 +BRDA:113,30,0,1 +BRDA:113,30,1,1 +BRF:62 +BRH:49 +end_of_record +TN: +SF:src/contexts/feedback360/application/listeners/review-stage.listener.ts +FN:35,(anonymous_4) +FN:48,(anonymous_5) +FN:92,(anonymous_6) +FNF:3 +FNH:3 +FNDA:6,(anonymous_4) +FNDA:6,(anonymous_5) +FNDA:6,(anonymous_6) +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +DA:20,1 +DA:32,1 +DA:33,6 +DA:36,6 +DA:38,6 +DA:39,6 +DA:48,1 +DA:51,6 +DA:55,6 +DA:56,6 +DA:57,1 +DA:60,5 +DA:62,5 +DA:63,1 +DA:64,1 +DA:67,4 +DA:68,1 +DA:71,1 +DA:74,3 +DA:78,3 +DA:79,3 +DA:92,3 +DA:93,6 +DA:95,3 +DA:97,3 +DA:98,2 +DA:99,1 +DA:103,1 +DA:108,1 +DA:119,6 +LF:37 +LH:37 +BRDA:56,0,0,1 +BRDA:56,0,1,5 +BRDA:62,1,0,1 +BRDA:62,1,1,4 +BRDA:67,2,0,1 +BRDA:67,2,1,3 +BRDA:97,3,0,2 +BRDA:97,3,1,1 +BRDA:49,4,0,1 +BRDA:49,4,1,0 +BRDA:49,5,0,1 +BRDA:49,5,1,1 +BRDA:50,6,0,1 +BRDA:50,6,1,0 +BRDA:50,7,0,1 +BRDA:50,7,1,1 +BRDA:36,8,0,1 +BRDA:36,8,1,0 +BRDA:36,9,0,1 +BRDA:36,9,1,1 +BRDA:36,10,0,0 +BRDA:36,10,1,1 +BRDA:36,11,0,1 +BRDA:36,11,1,0 +BRDA:38,12,0,1 +BRDA:38,12,1,0 +BRDA:38,13,0,1 +BRDA:38,13,1,1 +BRF:28 +BRH:22 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/answer.repository.port.ts +FNF:0 +FNH:0 +DA:4,18 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/cluster-score-analytics.repository.port.ts +FNF:0 +FNH:0 +DA:7,3 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/cluster-score.repository.port.ts +FNF:0 +FNH:0 +DA:5,15 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/cycle-stage-history.repository.port.ts +FNF:0 +FNH:0 +DA:3,18 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/cycle.repository.port.ts +FNF:0 +FNH:0 +DA:4,27 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/question.repository.port.ts +FNF:0 +FNH:0 +DA:4,14 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/respondent.repository.port.ts +FNF:0 +FNH:0 +DA:7,21 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/review-question-relation.repository.port.ts +FNF:0 +FNH:0 +DA:4,13 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/review-stage-history.repository.port.ts +FNF:0 +FNH:0 +DA:3,7 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/review.repository.port.ts +FNF:0 +FNH:0 +DA:4,27 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/ports/reviewer.repository.port.ts +FNF:0 +FNH:0 +DA:4,21 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/feedback360/application/services/cluster-score-analytics.service.ts +FN:28,(anonymous_5) +FN:37,(anonymous_6) +FN:65,(anonymous_7) +FN:71,(anonymous_8) +FN:80,(anonymous_9) +FN:122,(anonymous_10) +FN:127,(anonymous_11) +FN:137,(anonymous_12) +FN:151,(anonymous_13) +FN:159,(anonymous_14) +FN:164,(anonymous_15) +FN:170,(anonymous_16) +FN:171,(anonymous_17) +FN:174,(anonymous_18) +FN:175,(anonymous_19) +FN:209,(anonymous_20) +FN:231,(anonymous_21) +FNF:17 +FNH:17 +FNDA:18,(anonymous_5) +FNDA:6,(anonymous_6) +FNDA:1,(anonymous_7) +FNDA:6,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:1,(anonymous_11) +FNDA:4,(anonymous_12) +FNDA:10,(anonymous_13) +FNDA:7,(anonymous_14) +FNDA:7,(anonymous_15) +FNDA:6,(anonymous_16) +FNDA:6,(anonymous_17) +FNDA:10,(anonymous_18) +FNDA:10,(anonymous_19) +FNDA:8,(anonymous_20) +FNDA:10,(anonymous_21) +DA:1,3 +DA:7,3 +DA:13,3 +DA:14,3 +DA:15,3 +DA:16,3 +DA:20,3 +DA:24,3 +DA:27,3 +DA:30,18 +DA:32,18 +DA:33,18 +DA:34,18 +DA:40,6 +DA:41,6 +DA:42,6 +DA:43,6 +DA:44,6 +DA:45,6 +DA:47,6 +DA:48,3 +DA:50,1 +DA:62,1 +DA:68,1 +DA:72,6 +DA:73,6 +DA:74,2 +DA:77,4 +DA:84,2 +DA:87,2 +DA:91,2 +DA:95,2 +DA:99,2 +DA:101,1 +DA:119,1 +DA:123,2 +DA:124,1 +DA:130,1 +DA:138,4 +DA:140,4 +DA:141,4 +DA:145,4 +DA:146,5 +DA:147,0 +DA:150,5 +DA:151,10 +DA:154,5 +DA:155,1 +DA:158,4 +DA:159,7 +DA:161,4 +DA:162,4 +DA:163,4 +DA:164,7 +DA:167,4 +DA:169,4 +DA:170,6 +DA:171,6 +DA:172,4 +DA:174,10 +DA:175,10 +DA:177,4 +DA:181,4 +DA:182,4 +DA:184,4 +DA:185,3 +DA:187,3 +DA:199,3 +DA:214,8 +DA:215,2 +DA:219,6 +DA:220,1 +DA:235,10 +DA:236,2 +DA:240,8 +DA:241,1 +DA:245,7 +DA:246,1 +LF:78 +LH:77 +BRDA:73,0,0,2 +BRDA:73,0,1,4 +BRDA:87,1,0,1 +BRDA:87,1,1,1 +BRDA:91,2,0,0 +BRDA:91,2,1,2 +BRDA:95,3,0,0 +BRDA:95,3,1,2 +BRDA:102,4,0,1 +BRDA:102,4,1,0 +BRDA:105,5,0,0 +BRDA:105,5,1,1 +BRDA:108,6,0,0 +BRDA:108,6,1,1 +BRDA:111,7,0,0 +BRDA:111,7,1,1 +BRDA:114,8,0,0 +BRDA:114,8,1,1 +BRDA:146,9,0,0 +BRDA:146,9,1,5 +BRDA:154,10,0,1 +BRDA:154,10,1,4 +BRDA:214,11,0,2 +BRDA:214,11,1,6 +BRDA:219,12,0,1 +BRDA:219,12,1,5 +BRDA:219,13,0,6 +BRDA:219,13,1,6 +BRDA:235,14,0,2 +BRDA:235,14,1,8 +BRDA:240,15,0,1 +BRDA:240,15,1,7 +BRDA:245,16,0,1 +BRDA:245,16,1,6 +BRDA:30,17,0,0 +BRDA:30,17,1,3 +BRDA:30,18,0,3 +BRDA:30,18,1,0 +BRDA:30,19,0,0 +BRDA:30,19,1,3 +BRDA:30,20,0,3 +BRDA:30,20,1,0 +BRDA:32,21,0,3 +BRDA:32,21,1,0 +BRDA:32,22,0,3 +BRDA:32,22,1,3 +BRDA:33,23,0,3 +BRDA:33,23,1,0 +BRDA:33,24,0,3 +BRDA:33,24,1,3 +BRF:50 +BRH:36 +end_of_record +TN: +SF:src/contexts/feedback360/application/services/cycle.service.ts +FN:44,(anonymous_4) +FN:57,(anonymous_5) +FN:78,(anonymous_6) +FN:82,(anonymous_7) +FN:89,(anonymous_8) +FN:133,(anonymous_9) +FN:182,(anonymous_10) +FN:196,(anonymous_11) +FN:216,(anonymous_12) +FN:253,(anonymous_13) +FN:262,(anonymous_14) +FN:270,(anonymous_15) +FN:299,(anonymous_16) +FNF:13 +FNH:13 +FNDA:21,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:24,(anonymous_7) +FNDA:6,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:7,(anonymous_11) +FNDA:6,(anonymous_12) +FNDA:1,(anonymous_13) +FNDA:4,(anonymous_14) +FNDA:4,(anonymous_15) +FNDA:1,(anonymous_16) +DA:1,18 +DA:10,18 +DA:16,18 +DA:17,18 +DA:21,18 +DA:22,18 +DA:23,18 +DA:24,18 +DA:28,18 +DA:29,18 +DA:33,18 +DA:37,18 +DA:43,18 +DA:46,21 +DA:48,21 +DA:50,21 +DA:51,21 +DA:52,21 +DA:54,21 +DA:58,2 +DA:74,2 +DA:75,2 +DA:79,1 +DA:83,24 +DA:84,24 +DA:85,2 +DA:86,22 +DA:90,6 +DA:92,6 +DA:117,6 +DA:119,6 +DA:120,1 +DA:128,5 +DA:130,5 +DA:132,3 +DA:133,3 +DA:134,2 +DA:140,3 +DA:142,3 +DA:153,1 +DA:164,2 +DA:165,1 +DA:168,2 +DA:169,1 +DA:179,4 +DA:183,2 +DA:185,1 +DA:203,7 +DA:204,7 +DA:207,7 +DA:209,7 +DA:210,1 +DA:216,6 +DA:218,6 +DA:224,6 +DA:233,6 +DA:237,6 +DA:254,1 +DA:263,4 +DA:266,4 +DA:269,4 +DA:271,4 +DA:274,4 +DA:277,4 +DA:278,2 +DA:285,2 +DA:286,1 +DA:304,1 +DA:305,1 +LF:69 +LH:69 +BRDA:62,0,0,2 +BRDA:62,0,1,1 +BRDA:64,1,0,2 +BRDA:64,1,1,1 +BRDA:66,2,0,2 +BRDA:66,2,1,1 +BRDA:84,3,0,2 +BRDA:84,3,1,22 +BRDA:93,4,0,1 +BRDA:93,4,1,5 +BRDA:94,5,0,1 +BRDA:94,5,1,5 +BRDA:97,6,0,0 +BRDA:97,6,1,6 +BRDA:98,7,0,0 +BRDA:98,7,1,6 +BRDA:101,8,0,0 +BRDA:101,8,1,6 +BRDA:104,9,0,0 +BRDA:104,9,1,6 +BRDA:107,10,0,0 +BRDA:107,10,1,6 +BRDA:110,11,0,0 +BRDA:110,11,1,6 +BRDA:113,12,0,0 +BRDA:113,12,1,6 +BRDA:119,13,0,1 +BRDA:119,13,1,5 +BRDA:119,14,0,6 +BRDA:119,14,1,1 +BRDA:130,15,0,3 +BRDA:130,15,1,2 +BRDA:130,16,0,5 +BRDA:130,16,1,4 +BRDA:142,17,0,1 +BRDA:142,17,1,2 +BRDA:143,18,0,3 +BRDA:143,18,1,1 +BRDA:143,18,2,1 +BRDA:164,19,0,1 +BRDA:164,19,1,1 +BRDA:164,20,0,2 +BRDA:164,20,1,2 +BRDA:168,21,0,1 +BRDA:168,21,1,1 +BRDA:168,22,0,2 +BRDA:168,22,1,2 +BRDA:199,23,0,2 +BRDA:200,24,0,2 +BRDA:209,25,0,1 +BRDA:209,25,1,6 +BRDA:228,26,0,6 +BRDA:228,26,1,4 +BRDA:271,27,0,4 +BRDA:271,27,1,4 +BRDA:277,28,0,2 +BRDA:277,28,1,2 +BRDA:277,29,0,4 +BRDA:277,29,1,3 +BRDA:285,30,0,1 +BRDA:285,30,1,1 +BRDA:285,31,0,2 +BRDA:285,31,1,1 +BRDA:301,32,0,0 +BRDA:302,33,0,0 +BRDA:46,34,0,0 +BRDA:46,34,1,18 +BRDA:46,35,0,18 +BRDA:46,35,1,0 +BRDA:46,36,0,0 +BRDA:46,36,1,18 +BRDA:46,37,0,18 +BRDA:46,37,1,0 +BRDA:48,38,0,0 +BRDA:48,38,1,18 +BRDA:48,39,0,18 +BRDA:48,39,1,0 +BRDA:50,40,0,18 +BRDA:50,40,1,0 +BRDA:50,41,0,18 +BRDA:50,41,1,18 +BRDA:51,42,0,18 +BRDA:51,42,1,0 +BRDA:51,43,0,18 +BRDA:51,43,1,18 +BRDA:52,44,0,0 +BRDA:52,44,1,18 +BRDA:52,45,0,18 +BRDA:52,45,1,0 +BRF:89 +BRH:70 +end_of_record +TN: +SF:src/contexts/feedback360/application/services/review-scheduler.service.ts +FN:38,(anonymous_2) +FN:48,(anonymous_3) +FN:113,(anonymous_4) +FNF:3 +FNH:3 +FNDA:10,(anonymous_2) +FNDA:5,(anonymous_3) +FNDA:5,(anonymous_4) +DA:1,1 +DA:7,1 +DA:8,1 +DA:9,1 +DA:10,1 +DA:11,1 +DA:12,1 +DA:18,1 +DA:35,1 +DA:36,10 +DA:39,10 +DA:40,10 +DA:48,1 +DA:49,5 +DA:51,5 +DA:53,5 +DA:57,4 +DA:58,4 +DA:59,1 +DA:62,3 +DA:63,3 +DA:66,3 +DA:68,2 +DA:76,2 +DA:81,2 +DA:82,4 +DA:83,4 +DA:90,3 +DA:94,1 +DA:102,1 +DA:113,1 +DA:114,5 +DA:116,5 +DA:117,5 +DA:122,4 +DA:124,4 +DA:126,5 +DA:128,5 +DA:131,5 +DA:132,1 +DA:135,4 +DA:136,4 +DA:137,3 +DA:141,1 +DA:147,1 +LF:45 +LH:45 +BRDA:58,0,0,1 +BRDA:58,0,1,3 +BRDA:66,1,0,2 +BRDA:66,1,1,1 +BRDA:126,2,0,5 +BRDA:126,2,1,5 +BRDA:128,3,0,5 +BRDA:128,3,1,2 +BRDA:131,4,0,1 +BRDA:131,4,1,4 +BRDA:131,5,0,5 +BRDA:131,5,1,2 +BRDA:48,6,0,1 +BRDA:48,6,1,0 +BRDA:48,7,0,1 +BRDA:48,7,1,1 +BRDA:113,8,0,1 +BRDA:113,8,1,0 +BRDA:113,9,0,1 +BRDA:113,9,1,1 +BRDA:39,10,0,1 +BRDA:39,10,1,0 +BRDA:39,11,0,1 +BRDA:39,11,1,1 +BRDA:39,12,0,1 +BRDA:39,12,1,0 +BRDA:39,13,0,1 +BRDA:39,13,1,1 +BRF:28 +BRH:24 +end_of_record +TN: +SF:src/contexts/feedback360/application/services/review.service.ts +FN:91,(anonymous_4) +FN:115,(anonymous_5) +FN:143,(anonymous_6) +FN:151,(anonymous_7) +FN:159,(anonymous_8) +FN:164,(anonymous_9) +FN:180,(anonymous_10) +FN:208,(anonymous_11) +FN:246,(anonymous_12) +FN:280,(anonymous_13) +FN:380,(anonymous_14) +FN:395,(anonymous_15) +FN:442,(anonymous_16) +FN:446,(anonymous_17) +FN:473,(anonymous_18) +FN:529,(anonymous_19) +FN:538,(anonymous_20) +FN:553,(anonymous_21) +FN:583,(anonymous_22) +FN:594,(anonymous_23) +FN:604,(anonymous_24) +FN:607,(anonymous_25) +FN:648,(anonymous_26) +FN:697,(anonymous_27) +FN:746,(anonymous_28) +FN:767,(anonymous_29) +FN:783,(anonymous_30) +FN:787,(anonymous_31) +FN:801,(anonymous_32) +FN:816,(anonymous_33) +FN:854,(anonymous_34) +FN:858,(anonymous_35) +FN:877,(anonymous_36) +FN:883,(anonymous_37) +FN:889,(anonymous_38) +FN:895,(anonymous_39) +FN:907,(anonymous_40) +FN:927,(anonymous_41) +FN:964,(anonymous_42) +FN:975,(anonymous_43) +FN:983,(anonymous_44) +FN:1012,(anonymous_45) +FNF:42 +FNH:42 +FNDA:65,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:4,(anonymous_7) +FNDA:4,(anonymous_8) +FNDA:48,(anonymous_9) +FNDA:6,(anonymous_10) +FNDA:5,(anonymous_11) +FNDA:47,(anonymous_12) +FNDA:4,(anonymous_13) +FNDA:2,(anonymous_14) +FNDA:3,(anonymous_15) +FNDA:1,(anonymous_16) +FNDA:3,(anonymous_17) +FNDA:3,(anonymous_18) +FNDA:1,(anonymous_19) +FNDA:2,(anonymous_20) +FNDA:2,(anonymous_21) +FNDA:4,(anonymous_22) +FNDA:2,(anonymous_23) +FNDA:2,(anonymous_24) +FNDA:2,(anonymous_25) +FNDA:3,(anonymous_26) +FNDA:4,(anonymous_27) +FNDA:3,(anonymous_28) +FNDA:1,(anonymous_29) +FNDA:1,(anonymous_30) +FNDA:1,(anonymous_31) +FNDA:2,(anonymous_32) +FNDA:2,(anonymous_33) +FNDA:1,(anonymous_34) +FNDA:2,(anonymous_35) +FNDA:1,(anonymous_36) +FNDA:1,(anonymous_37) +FNDA:1,(anonymous_38) +FNDA:1,(anonymous_39) +FNDA:5,(anonymous_40) +FNDA:4,(anonymous_41) +FNDA:1,(anonymous_42) +FNDA:2,(anonymous_43) +FNDA:3,(anonymous_44) +FNDA:1,(anonymous_45) +DA:1,7 +DA:24,7 +DA:31,7 +DA:33,7 +DA:34,7 +DA:35,7 +DA:36,7 +DA:38,7 +DA:39,7 +DA:40,7 +DA:41,7 +DA:42,7 +DA:43,7 +DA:44,7 +DA:45,7 +DA:49,7 +DA:53,7 +DA:54,7 +DA:55,7 +DA:59,7 +DA:63,7 +DA:67,7 +DA:71,7 +DA:75,7 +DA:79,7 +DA:83,7 +DA:87,7 +DA:90,7 +DA:93,65 +DA:95,65 +DA:97,65 +DA:99,65 +DA:101,65 +DA:103,65 +DA:105,65 +DA:107,65 +DA:108,65 +DA:109,65 +DA:110,65 +DA:111,65 +DA:112,65 +DA:116,2 +DA:117,1 +DA:120,2 +DA:139,2 +DA:140,2 +DA:147,2 +DA:149,2 +DA:152,4 +DA:156,4 +DA:159,4 +DA:161,2 +DA:165,48 +DA:166,48 +DA:167,1 +DA:169,47 +DA:171,46 +DA:184,6 +DA:187,6 +DA:190,6 +DA:192,4 +DA:193,4 +DA:195,4 +DA:197,2 +DA:201,2 +DA:203,1 +DA:212,5 +DA:215,5 +DA:218,5 +DA:220,2 +DA:221,2 +DA:223,2 +DA:225,1 +DA:229,1 +DA:231,1 +DA:235,1 +DA:237,1 +DA:250,47 +DA:253,9 +DA:256,9 +DA:258,5 +DA:259,5 +DA:261,5 +DA:263,3 +DA:267,3 +DA:269,2 +DA:273,2 +DA:275,1 +DA:285,4 +DA:287,4 +DA:291,1 +DA:299,3 +DA:300,1 +DA:301,1 +DA:305,1 +DA:314,2 +DA:352,2 +DA:355,2 +DA:360,0 +DA:363,2 +DA:368,1 +DA:377,2 +DA:381,2 +DA:383,2 +DA:384,2 +DA:385,1 +DA:392,1 +DA:398,3 +DA:399,2 +DA:400,2 +DA:404,1 +DA:413,2 +DA:417,2 +DA:418,2 +DA:420,2 +DA:421,1 +DA:426,1 +DA:439,1 +DA:443,1 +DA:447,3 +DA:449,3 +DA:450,1 +DA:455,2 +DA:456,1 +DA:457,1 +DA:461,1 +DA:470,1 +DA:476,3 +DA:478,3 +DA:479,1 +DA:487,2 +DA:490,2 +DA:497,2 +DA:502,2 +DA:514,2 +DA:526,2 +DA:534,1 +DA:535,1 +DA:539,2 +DA:541,2 +DA:542,1 +DA:550,1 +DA:557,2 +DA:559,2 +DA:563,1 +DA:571,1 +DA:580,1 +DA:588,4 +DA:589,4 +DA:591,3 +DA:598,2 +DA:599,2 +DA:602,2 +DA:604,2 +DA:610,2 +DA:612,2 +DA:618,1 +DA:628,1 +DA:645,1 +DA:653,3 +DA:655,2 +DA:656,2 +DA:658,2 +DA:662,1 +DA:670,1 +DA:671,1 +DA:673,1 +DA:675,1 +DA:676,1 +DA:686,1 +DA:705,4 +DA:706,4 +DA:707,4 +DA:708,1 +DA:712,3 +DA:715,3 +DA:716,3 +DA:717,1 +DA:722,2 +DA:726,2 +DA:750,3 +DA:752,3 +DA:754,3 +DA:756,3 +DA:758,3 +DA:760,1 +DA:772,1 +DA:774,1 +DA:775,0 +DA:780,1 +DA:784,1 +DA:788,1 +DA:789,1 +DA:798,1 +DA:806,2 +DA:807,1 +DA:820,2 +DA:823,2 +DA:826,2 +DA:828,1 +DA:830,1 +DA:831,1 +DA:833,1 +DA:835,1 +DA:839,1 +DA:841,1 +DA:845,1 +DA:847,1 +DA:855,1 +DA:861,2 +DA:862,1 +DA:865,2 +DA:874,2 +DA:880,1 +DA:886,1 +DA:892,1 +DA:896,1 +DA:914,5 +DA:915,5 +DA:918,5 +DA:920,5 +DA:921,1 +DA:927,4 +DA:929,4 +DA:935,4 +DA:944,4 +DA:948,4 +DA:967,1 +DA:976,2 +DA:979,2 +DA:982,2 +DA:984,3 +DA:987,2 +DA:990,2 +DA:991,1 +DA:999,1 +DA:1017,1 +LF:237 +LH:235 +BRDA:116,0,0,1 +BRDA:116,0,1,1 +BRDA:128,1,0,2 +BRDA:128,1,1,2 +BRDA:129,2,0,2 +BRDA:129,2,1,2 +BRDA:130,3,0,2 +BRDA:130,3,1,2 +BRDA:131,4,0,2 +BRDA:131,4,1,2 +BRDA:132,5,0,2 +BRDA:132,5,1,2 +BRDA:133,6,0,2 +BRDA:133,6,1,2 +BRDA:134,7,0,2 +BRDA:134,7,1,1 +BRDA:135,8,0,2 +BRDA:135,8,1,2 +BRDA:136,9,0,2 +BRDA:136,9,1,2 +BRDA:156,10,0,3 +BRDA:156,10,1,1 +BRDA:166,11,0,1 +BRDA:166,11,1,47 +BRDA:184,12,0,0 +BRDA:184,12,1,6 +BRDA:187,13,0,6 +BRDA:187,13,1,6 +BRDA:190,14,0,2 +BRDA:190,14,1,4 +BRDA:195,15,0,2 +BRDA:195,15,1,2 +BRDA:195,16,0,4 +BRDA:195,16,1,3 +BRDA:201,17,0,1 +BRDA:201,17,1,1 +BRDA:212,18,0,0 +BRDA:212,18,1,5 +BRDA:215,19,0,5 +BRDA:215,19,1,5 +BRDA:218,20,0,3 +BRDA:218,20,1,2 +BRDA:223,21,0,1 +BRDA:223,21,1,1 +BRDA:223,22,0,2 +BRDA:223,22,1,2 +BRDA:229,23,0,0 +BRDA:229,23,1,1 +BRDA:235,24,0,0 +BRDA:235,24,1,1 +BRDA:250,25,0,38 +BRDA:250,25,1,9 +BRDA:253,26,0,9 +BRDA:253,26,1,8 +BRDA:256,27,0,4 +BRDA:256,27,1,5 +BRDA:261,28,0,2 +BRDA:261,28,1,3 +BRDA:261,29,0,5 +BRDA:261,29,1,4 +BRDA:267,30,0,1 +BRDA:267,30,1,2 +BRDA:273,31,0,1 +BRDA:273,31,1,1 +BRDA:287,32,0,1 +BRDA:287,32,1,3 +BRDA:288,33,0,4 +BRDA:288,33,1,1 +BRDA:299,34,0,1 +BRDA:299,34,1,2 +BRDA:301,35,0,1 +BRDA:301,35,1,0 +BRDA:302,36,0,1 +BRDA:302,36,1,1 +BRDA:315,37,0,0 +BRDA:315,37,1,2 +BRDA:316,38,0,0 +BRDA:316,38,1,2 +BRDA:319,39,0,0 +BRDA:319,39,1,2 +BRDA:322,40,0,0 +BRDA:322,40,1,2 +BRDA:325,41,0,0 +BRDA:325,41,1,2 +BRDA:326,42,0,0 +BRDA:326,42,1,2 +BRDA:329,43,0,1 +BRDA:329,43,1,1 +BRDA:330,44,0,0 +BRDA:330,44,1,2 +BRDA:331,45,0,0 +BRDA:331,45,1,2 +BRDA:334,46,0,0 +BRDA:334,46,1,2 +BRDA:337,47,0,0 +BRDA:337,47,1,2 +BRDA:340,48,0,0 +BRDA:340,48,1,2 +BRDA:343,49,0,0 +BRDA:343,49,1,2 +BRDA:346,50,0,0 +BRDA:346,50,1,2 +BRDA:347,51,0,0 +BRDA:347,51,1,2 +BRDA:355,52,0,0 +BRDA:355,52,1,2 +BRDA:356,53,0,2 +BRDA:356,53,1,1 +BRDA:356,53,2,1 +BRDA:363,54,0,1 +BRDA:363,54,1,1 +BRDA:364,55,0,2 +BRDA:364,55,1,1 +BRDA:364,55,2,1 +BRDA:384,56,0,1 +BRDA:384,56,1,1 +BRDA:398,57,0,2 +BRDA:398,57,1,1 +BRDA:400,58,0,1 +BRDA:400,58,1,1 +BRDA:401,59,0,2 +BRDA:401,59,1,2 +BRDA:413,60,0,1 +BRDA:413,60,1,1 +BRDA:417,61,0,2 +BRDA:417,61,1,2 +BRDA:418,62,0,2 +BRDA:418,62,1,2 +BRDA:420,63,0,1 +BRDA:420,63,1,1 +BRDA:420,64,0,2 +BRDA:420,64,1,1 +BRDA:427,65,0,1 +BRDA:427,65,1,0 +BRDA:428,66,0,1 +BRDA:428,66,1,0 +BRDA:432,67,0,1 +BRDA:432,67,1,1 +BRDA:432,67,2,0 +BRDA:434,68,0,1 +BRDA:434,68,1,1 +BRDA:434,68,2,0 +BRDA:449,69,0,1 +BRDA:449,69,1,2 +BRDA:455,70,0,1 +BRDA:455,70,1,1 +BRDA:457,71,0,1 +BRDA:457,71,1,0 +BRDA:458,72,0,1 +BRDA:458,72,1,1 +BRDA:478,73,0,1 +BRDA:478,73,1,2 +BRDA:498,74,0,2 +BRDA:498,74,1,0 +BRDA:502,75,0,2 +BRDA:502,75,1,1 +BRDA:505,76,0,1 +BRDA:505,76,1,0 +BRDA:541,77,0,1 +BRDA:541,77,1,1 +BRDA:559,78,0,1 +BRDA:559,78,1,1 +BRDA:560,79,0,2 +BRDA:560,79,1,1 +BRDA:576,80,0,1 +BRDA:576,80,1,0 +BRDA:577,81,0,1 +BRDA:577,81,1,1 +BRDA:612,82,0,1 +BRDA:612,82,1,1 +BRDA:613,83,0,2 +BRDA:613,83,1,2 +BRDA:613,83,2,1 +BRDA:613,83,3,1 +BRDA:658,84,0,1 +BRDA:658,84,1,1 +BRDA:659,85,0,2 +BRDA:659,85,1,1 +BRDA:675,86,0,1 +BRDA:675,86,1,0 +BRDA:701,87,0,2 +BRDA:702,88,0,2 +BRDA:703,89,0,2 +BRDA:707,90,0,1 +BRDA:707,90,1,3 +BRDA:716,91,0,1 +BRDA:716,91,1,2 +BRDA:750,92,0,0 +BRDA:750,92,1,3 +BRDA:754,93,0,0 +BRDA:754,93,1,3 +BRDA:758,94,0,2 +BRDA:758,94,1,1 +BRDA:774,95,0,0 +BRDA:774,95,1,1 +BRDA:820,96,0,0 +BRDA:820,96,1,2 +BRDA:823,97,0,2 +BRDA:823,97,1,2 +BRDA:826,98,0,1 +BRDA:826,98,1,1 +BRDA:833,99,0,0 +BRDA:833,99,1,1 +BRDA:833,100,0,1 +BRDA:833,100,1,1 +BRDA:839,101,0,0 +BRDA:839,101,1,1 +BRDA:845,102,0,0 +BRDA:845,102,1,1 +BRDA:861,103,0,1 +BRDA:861,103,1,1 +BRDA:866,104,0,2 +BRDA:866,104,1,1 +BRDA:871,105,0,2 +BRDA:871,105,1,2 +BRDA:910,106,0,1 +BRDA:911,107,0,1 +BRDA:920,108,0,1 +BRDA:920,108,1,4 +BRDA:939,109,0,4 +BRDA:939,109,1,1 +BRDA:984,110,0,3 +BRDA:984,110,1,2 +BRDA:990,111,0,1 +BRDA:990,111,1,1 +BRDA:990,112,0,2 +BRDA:990,112,1,1 +BRDA:93,113,0,0 +BRDA:93,113,1,7 +BRDA:93,114,0,7 +BRDA:93,114,1,0 +BRDA:93,115,0,0 +BRDA:93,115,1,7 +BRDA:93,116,0,7 +BRDA:93,116,1,0 +BRDA:95,117,0,0 +BRDA:95,117,1,7 +BRDA:95,118,0,7 +BRDA:95,118,1,0 +BRDA:97,119,0,0 +BRDA:97,119,1,7 +BRDA:97,120,0,7 +BRDA:97,120,1,0 +BRDA:99,121,0,0 +BRDA:99,121,1,7 +BRDA:99,122,0,7 +BRDA:99,122,1,0 +BRDA:101,123,0,0 +BRDA:101,123,1,7 +BRDA:101,124,0,7 +BRDA:101,124,1,0 +BRDA:103,125,0,0 +BRDA:103,125,1,7 +BRDA:103,126,0,7 +BRDA:103,126,1,0 +BRDA:105,127,0,0 +BRDA:105,127,1,7 +BRDA:105,128,0,7 +BRDA:105,128,1,0 +BRDA:107,129,0,7 +BRDA:107,129,1,0 +BRDA:107,130,0,7 +BRDA:107,130,1,7 +BRDA:108,131,0,7 +BRDA:108,131,1,0 +BRDA:108,132,0,7 +BRDA:108,132,1,7 +BRDA:109,133,0,7 +BRDA:109,133,1,0 +BRDA:109,134,0,7 +BRDA:109,134,1,7 +BRDA:110,135,0,7 +BRDA:110,135,1,0 +BRDA:110,136,0,7 +BRDA:110,136,1,7 +BRDA:111,137,0,7 +BRDA:111,137,1,0 +BRDA:111,138,0,7 +BRDA:111,138,1,7 +BRF:279 +BRH:222 +end_of_record +TN: +SF:src/contexts/identity/application/events/user-created.event.ts +FN:7,(anonymous_0) +FNF:1 +FNH:1 +FNDA:7,(anonymous_0) +DA:6,18 +DA:8,7 +DA:9,7 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/identity/application/ports/role.repository.port.ts +FNF:0 +FNH:0 +DA:4,17 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/identity/application/ports/user.repository.port.ts +FNF:0 +FNH:0 +DA:8,24 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/identity/application/services/identity-role.service.ts +FN:9,(anonymous_4) +FN:14,(anonymous_5) +FN:18,(anonymous_6) +FNF:3 +FNH:3 +FNDA:2,(anonymous_4) +FNDA:1,(anonymous_5) +FNDA:1,(anonymous_6) +DA:2,2 +DA:5,2 +DA:8,2 +DA:11,2 +DA:15,1 +DA:19,1 +LF:6 +LH:6 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/identity/application/services/identity-user.service.ts +FN:26,(anonymous_4) +FN:34,(anonymous_5) +FN:77,(anonymous_6) +FN:81,(anonymous_7) +FN:90,(anonymous_8) +FN:97,(anonymous_9) +FN:120,(anonymous_10) +FN:125,(anonymous_11) +FN:134,(anonymous_12) +FN:135,(anonymous_13) +FN:144,(anonymous_14) +FN:189,(anonymous_15) +FNF:12 +FNH:12 +FNDA:19,(anonymous_4) +FNDA:5,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:10,(anonymous_7) +FNDA:1,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:2,(anonymous_11) +FNDA:1,(anonymous_12) +FNDA:2,(anonymous_13) +FNDA:2,(anonymous_14) +FNDA:6,(anonymous_15) +DA:1,16 +DA:8,16 +DA:14,16 +DA:15,16 +DA:16,16 +DA:18,16 +DA:20,16 +DA:25,16 +DA:28,19 +DA:30,19 +DA:31,19 +DA:35,5 +DA:55,5 +DA:59,5 +DA:60,1 +DA:65,4 +DA:67,4 +DA:68,3 +DA:74,4 +DA:78,1 +DA:85,10 +DA:86,10 +DA:87,7 +DA:94,1 +DA:98,3 +DA:100,2 +DA:104,2 +DA:112,2 +DA:117,2 +DA:121,2 +DA:122,1 +DA:129,2 +DA:131,2 +DA:132,2 +DA:133,2 +DA:134,1 +DA:135,2 +DA:136,1 +DA:141,1 +DA:151,2 +DA:155,2 +DA:156,1 +DA:159,1 +DA:177,1 +DA:179,1 +DA:180,1 +DA:186,1 +DA:194,6 +DA:197,6 +LF:49 +LH:49 +BRDA:40,0,0,5 +BRDA:40,0,1,4 +BRDA:48,1,0,5 +BRDA:48,1,1,5 +BRDA:49,2,0,5 +BRDA:49,2,1,5 +BRDA:50,3,0,5 +BRDA:50,3,1,5 +BRDA:51,4,0,5 +BRDA:51,4,1,5 +BRDA:52,5,0,5 +BRDA:52,5,1,5 +BRDA:59,6,0,1 +BRDA:59,6,1,4 +BRDA:67,7,0,3 +BRDA:67,7,1,1 +BRDA:86,8,0,3 +BRDA:86,8,1,7 +BRDA:100,9,0,2 +BRDA:100,9,1,1 +BRDA:100,9,2,1 +BRDA:104,10,0,1 +BRDA:104,10,1,1 +BRDA:106,11,0,1 +BRDA:106,11,1,1 +BRDA:107,12,0,1 +BRDA:107,12,1,0 +BRDA:108,13,0,1 +BRDA:108,13,1,1 +BRDA:114,14,0,1 +BRDA:114,14,1,1 +BRDA:133,15,0,1 +BRDA:133,15,1,1 +BRDA:155,16,0,1 +BRDA:155,16,1,1 +BRDA:179,17,0,1 +BRDA:179,17,1,0 +BRDA:194,18,0,6 +BRDA:194,18,1,5 +BRDA:31,19,0,16 +BRDA:31,19,1,0 +BRDA:31,20,0,16 +BRDA:31,20,1,16 +BRF:43 +BRH:40 +end_of_record +TN: +SF:src/contexts/library/application/ports/cluster.repository.port.ts +FNF:0 +FNH:0 +DA:4,11 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/library/application/ports/competence-question-template-relation.repository.port.ts +FNF:0 +FNH:0 +DA:3,16 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/library/application/ports/competence.repository.port.ts +FNF:0 +FNH:0 +DA:7,21 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/library/application/ports/position-competence-relation.repository.port.ts +FNF:0 +FNH:0 +DA:3,19 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/library/application/ports/position-question-template-relation.repository.port.ts +FNF:0 +FNH:0 +DA:3,9 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/library/application/ports/question-template.repository.port.ts +FNF:0 +FNH:0 +DA:7,16 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/library/application/services/cluster.service.ts +FN:21,(anonymous_4) +FN:27,(anonymous_5) +FN:42,(anonymous_6) +FN:46,(anonymous_7) +FN:52,(anonymous_8) +FN:87,(anonymous_9) +FN:92,(anonymous_10) +FNF:7 +FNH:7 +FNDA:12,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:9,(anonymous_7) +FNDA:5,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:3,(anonymous_10) +DA:6,5 +DA:12,5 +DA:13,5 +DA:17,5 +DA:20,5 +DA:23,12 +DA:24,12 +DA:28,2 +DA:29,1 +DA:31,1 +DA:39,1 +DA:43,1 +DA:47,9 +DA:48,9 +DA:49,6 +DA:56,5 +DA:58,4 +DA:59,1 +DA:62,4 +DA:63,1 +DA:64,1 +DA:65,1 +DA:68,3 +DA:84,3 +DA:88,2 +DA:89,1 +DA:93,3 +DA:94,2 +LF:28 +LH:28 +BRDA:48,0,0,3 +BRDA:48,0,1,6 +BRDA:58,1,0,1 +BRDA:58,1,1,3 +BRDA:58,2,0,4 +BRDA:58,2,1,2 +BRDA:62,3,0,1 +BRDA:62,3,1,3 +BRDA:62,4,0,4 +BRDA:62,4,1,3 +BRDA:63,5,0,1 +BRDA:63,5,1,0 +BRDA:64,6,0,1 +BRDA:64,6,1,1 +BRDA:69,7,0,2 +BRDA:69,7,1,1 +BRDA:72,8,0,0 +BRDA:72,8,1,3 +BRDA:75,9,0,0 +BRDA:75,9,1,3 +BRDA:78,10,0,1 +BRDA:78,10,1,2 +BRDA:79,11,0,0 +BRDA:79,11,1,3 +BRDA:93,12,0,2 +BRDA:93,12,1,1 +BRDA:23,13,0,0 +BRDA:23,13,1,5 +BRDA:23,14,0,5 +BRDA:23,14,1,0 +BRDA:23,15,0,5 +BRDA:23,15,1,0 +BRDA:23,16,0,5 +BRDA:23,16,1,5 +BRF:34 +BRH:27 +end_of_record +TN: +SF:src/contexts/library/application/services/competence.service.ts +FN:33,(anonymous_4) +FN:45,(anonymous_5) +FN:54,(anonymous_6) +FN:58,(anonymous_7) +FN:64,(anonymous_8) +FN:79,(anonymous_9) +FN:104,(anonymous_10) +FN:115,(anonymous_11) +FN:123,(anonymous_12) +FN:129,(anonymous_13) +FN:132,(anonymous_14) +FN:146,(anonymous_15) +FN:157,(anonymous_16) +FN:163,(anonymous_17) +FN:166,(anonymous_18) +FNF:15 +FNH:15 +FNDA:17,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:22,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:1,(anonymous_10) +FNDA:1,(anonymous_11) +FNDA:5,(anonymous_12) +FNDA:4,(anonymous_13) +FNDA:2,(anonymous_14) +FNDA:1,(anonymous_15) +FNDA:5,(anonymous_16) +FNDA:4,(anonymous_17) +FNDA:2,(anonymous_18) +DA:6,16 +DA:12,16 +DA:13,16 +DA:14,16 +DA:18,16 +DA:22,16 +DA:26,16 +DA:32,16 +DA:35,17 +DA:37,17 +DA:39,17 +DA:41,17 +DA:42,17 +DA:46,2 +DA:51,2 +DA:55,1 +DA:59,22 +DA:60,22 +DA:61,20 +DA:68,2 +DA:69,1 +DA:76,1 +DA:80,3 +DA:81,3 +DA:82,3 +DA:83,3 +DA:87,2 +DA:97,1 +DA:98,1 +DA:101,1 +DA:108,1 +DA:109,1 +DA:111,1 +DA:112,1 +DA:119,1 +DA:120,1 +DA:124,5 +DA:126,5 +DA:129,5 +DA:136,2 +DA:137,2 +DA:139,1 +DA:143,1 +DA:150,1 +DA:151,1 +DA:158,5 +DA:160,5 +DA:163,5 +DA:169,2 +DA:170,2 +LF:50 +LH:50 +BRDA:47,0,0,2 +BRDA:47,0,1,1 +BRDA:49,1,0,2 +BRDA:49,1,1,1 +BRDA:60,2,0,2 +BRDA:60,2,1,20 +BRDA:70,3,0,0 +BRDA:70,3,1,1 +BRDA:71,4,0,1 +BRDA:71,4,1,0 +BRDA:72,5,0,0 +BRDA:72,5,1,1 +BRDA:83,6,0,2 +BRDA:83,6,1,1 +BRDA:84,7,0,3 +BRDA:84,7,1,2 +BRDA:170,8,0,1 +BRDA:170,8,1,1 +BRDA:35,9,0,0 +BRDA:35,9,1,16 +BRDA:35,10,0,16 +BRDA:35,10,1,0 +BRDA:35,11,0,0 +BRDA:35,11,1,16 +BRDA:35,12,0,16 +BRDA:35,12,1,0 +BRDA:37,13,0,0 +BRDA:37,13,1,16 +BRDA:37,14,0,16 +BRDA:37,14,1,0 +BRDA:39,15,0,0 +BRDA:39,15,1,16 +BRDA:39,16,0,16 +BRDA:39,16,1,0 +BRDA:41,17,0,16 +BRDA:41,17,1,0 +BRDA:41,18,0,16 +BRDA:41,18,1,16 +BRF:38 +BRH:26 +end_of_record +TN: +SF:src/contexts/library/application/services/question-template.service.ts +FN:35,(anonymous_4) +FN:48,(anonymous_5) +FN:78,(anonymous_6) +FN:84,(anonymous_7) +FN:91,(anonymous_8) +FN:132,(anonymous_9) +FN:155,(anonymous_10) +FN:169,(anonymous_11) +FN:180,(anonymous_12) +FN:186,(anonymous_13) +FN:189,(anonymous_14) +FN:203,(anonymous_15) +FN:214,(anonymous_16) +FN:220,(anonymous_17) +FN:223,(anonymous_18) +FN:224,(anonymous_19) +FNF:16 +FNH:16 +FNDA:16,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:20,(anonymous_7) +FNDA:3,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:1,(anonymous_10) +FNDA:1,(anonymous_11) +FNDA:2,(anonymous_12) +FNDA:3,(anonymous_13) +FNDA:1,(anonymous_14) +FNDA:1,(anonymous_15) +FNDA:2,(anonymous_16) +FNDA:3,(anonymous_17) +FNDA:2,(anonymous_18) +FNDA:4,(anonymous_19) +DA:1,9 +DA:7,9 +DA:13,9 +DA:17,9 +DA:18,9 +DA:19,9 +DA:23,9 +DA:27,9 +DA:31,9 +DA:34,9 +DA:37,16 +DA:39,16 +DA:41,16 +DA:42,16 +DA:43,16 +DA:45,16 +DA:51,2 +DA:53,2 +DA:62,2 +DA:64,2 +DA:67,2 +DA:68,1 +DA:69,1 +DA:75,2 +DA:81,1 +DA:85,20 +DA:86,20 +DA:87,1 +DA:88,19 +DA:95,3 +DA:97,3 +DA:98,1 +DA:101,3 +DA:118,3 +DA:120,3 +DA:121,1 +DA:122,1 +DA:123,1 +DA:129,3 +DA:133,2 +DA:134,2 +DA:137,2 +DA:138,1 +DA:146,1 +DA:149,1 +DA:152,1 +DA:159,1 +DA:160,1 +DA:162,1 +DA:166,1 +DA:173,1 +DA:174,1 +DA:181,2 +DA:183,2 +DA:186,3 +DA:193,1 +DA:194,1 +DA:196,1 +DA:200,1 +DA:207,1 +DA:208,1 +DA:215,2 +DA:217,2 +DA:220,3 +DA:224,4 +LF:65 +LH:65 +BRDA:57,0,0,2 +BRDA:57,0,1,2 +BRDA:58,1,0,2 +BRDA:58,1,1,2 +BRDA:59,2,0,2 +BRDA:59,2,1,1 +BRDA:64,3,0,1 +BRDA:64,3,1,1 +BRDA:67,4,0,1 +BRDA:67,4,1,1 +BRDA:86,5,0,1 +BRDA:86,5,1,19 +BRDA:97,6,0,1 +BRDA:97,6,1,2 +BRDA:97,7,0,3 +BRDA:97,7,1,1 +BRDA:102,8,0,1 +BRDA:102,8,1,2 +BRDA:105,9,0,1 +BRDA:105,9,1,2 +BRDA:106,10,0,0 +BRDA:106,10,1,3 +BRDA:109,11,0,0 +BRDA:109,11,1,3 +BRDA:112,12,0,0 +BRDA:112,12,1,3 +BRDA:113,13,0,1 +BRDA:113,13,1,2 +BRDA:120,14,0,1 +BRDA:120,14,1,2 +BRDA:137,15,0,1 +BRDA:137,15,1,1 +BRDA:37,16,0,0 +BRDA:37,16,1,9 +BRDA:37,17,0,9 +BRDA:37,17,1,0 +BRDA:37,18,0,0 +BRDA:37,18,1,9 +BRDA:37,19,0,9 +BRDA:37,19,1,0 +BRDA:39,20,0,0 +BRDA:39,20,1,9 +BRDA:39,21,0,9 +BRDA:39,21,1,0 +BRDA:41,22,0,9 +BRDA:41,22,1,0 +BRDA:41,23,0,9 +BRDA:41,23,1,9 +BRDA:42,24,0,9 +BRDA:42,24,1,0 +BRDA:42,25,0,9 +BRDA:42,25,1,9 +BRDA:43,26,0,0 +BRDA:43,26,1,9 +BRDA:43,27,0,9 +BRDA:43,27,1,0 +BRF:56 +BRH:43 +end_of_record +TN: +SF:src/contexts/notifications/application/listeners/cycle-stage-notification.listener.ts +FN:11,(anonymous_2) +FN:16,(anonymous_3) +FNF:2 +FNH:2 +FNDA:3,(anonymous_2) +FNDA:3,(anonymous_3) +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:8,1 +DA:9,3 +DA:12,3 +DA:16,1 +DA:17,3 +DA:19,3 +DA:20,3 +DA:23,2 +DA:26,1 +DA:29,1 +DA:32,1 +DA:36,1 +DA:37,1 +LF:18 +LH:18 +BRDA:20,0,0,2 +BRDA:20,0,1,1 +BRDA:36,1,0,1 +BRDA:36,1,1,0 +BRDA:39,2,0,1 +BRDA:39,2,1,0 +BRDA:16,3,0,1 +BRDA:16,3,1,0 +BRDA:16,4,0,1 +BRDA:16,4,1,1 +BRDA:16,5,0,1 +BRDA:16,5,1,0 +BRDA:16,6,0,1 +BRDA:16,6,1,1 +BRDA:12,7,0,1 +BRDA:12,7,1,0 +BRDA:12,8,0,1 +BRDA:12,8,1,1 +BRF:18 +BRH:13 +end_of_record +TN: +SF:src/contexts/notifications/application/listeners/review-stage-notification.listener.ts +FN:11,(anonymous_2) +FN:16,(anonymous_3) +FNF:2 +FNH:2 +FNDA:6,(anonymous_2) +FNDA:6,(anonymous_3) +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:8,1 +DA:9,6 +DA:12,6 +DA:16,1 +DA:17,6 +DA:19,6 +DA:20,6 +DA:23,1 +DA:26,1 +DA:29,1 +DA:33,2 +DA:34,1 +DA:37,1 +DA:41,1 +DA:42,1 +DA:45,1 +DA:49,1 +DA:50,1 +DA:53,1 +DA:56,1 +DA:60,1 +DA:61,1 +LF:27 +LH:27 +BRDA:20,0,0,1 +BRDA:20,0,1,2 +BRDA:20,0,2,1 +BRDA:20,0,3,1 +BRDA:20,0,4,1 +BRDA:60,1,0,1 +BRDA:60,1,1,0 +BRDA:63,2,0,1 +BRDA:63,2,1,0 +BRDA:16,3,0,1 +BRDA:16,3,1,0 +BRDA:16,4,0,1 +BRDA:16,4,1,1 +BRDA:16,5,0,1 +BRDA:16,5,1,0 +BRDA:16,6,0,1 +BRDA:16,6,1,1 +BRDA:12,7,0,1 +BRDA:12,7,1,0 +BRDA:12,8,0,1 +BRDA:12,8,1,1 +BRF:21 +BRH:16 +end_of_record +TN: +SF:src/contexts/notifications/application/listeners/user-created-notification.listener.ts +FN:10,(anonymous_2) +FN:15,(anonymous_3) +FNF:2 +FNH:2 +FNDA:2,(anonymous_2) +FNDA:2,(anonymous_3) +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:7,1 +DA:8,2 +DA:11,2 +DA:15,1 +DA:16,2 +DA:18,2 +DA:19,2 +DA:20,1 +DA:25,1 +DA:26,1 +LF:14 +LH:14 +BRDA:25,0,0,1 +BRDA:25,0,1,0 +BRDA:28,1,0,1 +BRDA:28,1,1,0 +BRDA:15,2,0,1 +BRDA:15,2,1,0 +BRDA:15,3,0,1 +BRDA:15,3,1,1 +BRDA:15,4,0,1 +BRDA:15,4,1,0 +BRDA:15,5,0,1 +BRDA:15,5,1,1 +BRDA:11,6,0,1 +BRDA:11,6,1,0 +BRDA:11,7,0,1 +BRDA:11,7,1,1 +BRF:16 +BRH:11 +end_of_record +TN: +SF:src/contexts/notifications/application/ports/mailer.port.ts +FNF:0 +FNH:0 +DA:1,5 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/notifications/application/ports/notification-log.repository.port.ts +FNF:0 +FNH:0 +DA:4,4 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/notifications/application/services/review-email-notification.service.ts +FN:43,(anonymous_4) +FN:69,(anonymous_5) +FN:96,(anonymous_6) +FN:125,(anonymous_7) +FN:155,(anonymous_8) +FN:200,(anonymous_9) +FN:233,(anonymous_10) +FN:276,(anonymous_11) +FN:387,(anonymous_12) +FNF:9 +FNH:9 +FNDA:19,(anonymous_4) +FNDA:8,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:3,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:12,(anonymous_11) +FNDA:12,(anonymous_12) +DA:1,4 +DA:2,4 +DA:3,4 +DA:7,4 +DA:11,4 +DA:15,4 +DA:16,4 +DA:21,4 +DA:25,4 +DA:26,4 +DA:27,4 +DA:33,4 +DA:34,4 +DA:40,4 +DA:41,19 +DA:44,19 +DA:46,19 +DA:48,19 +DA:50,19 +DA:52,19 +DA:54,19 +DA:56,19 +DA:57,19 +DA:58,19 +DA:70,8 +DA:71,8 +DA:72,1 +DA:73,1 +DA:76,7 +DA:88,7 +DA:97,2 +DA:99,2 +DA:101,2 +DA:102,2 +DA:103,1 +DA:104,1 +DA:107,1 +DA:122,1 +DA:126,3 +DA:127,3 +DA:128,1 +DA:129,1 +DA:132,2 +DA:133,2 +DA:134,1 +DA:137,1 +DA:140,1 +DA:152,1 +DA:156,2 +DA:157,2 +DA:158,1 +DA:159,1 +DA:162,1 +DA:167,1 +DA:169,1 +DA:170,2 +DA:171,2 +DA:172,1 +DA:175,1 +DA:178,1 +DA:192,1 +DA:193,1 +DA:197,1 +DA:201,3 +DA:202,3 +DA:203,1 +DA:204,1 +DA:207,2 +DA:208,2 +DA:209,1 +DA:212,1 +DA:215,1 +DA:230,1 +DA:234,2 +DA:235,2 +DA:236,1 +DA:237,1 +DA:240,1 +DA:242,1 +DA:244,1 +DA:245,1 +DA:246,1 +DA:247,0 +DA:250,0 +DA:253,1 +DA:268,1 +DA:269,1 +DA:273,1 +DA:288,12 +DA:290,12 +DA:291,0 +DA:292,0 +DA:294,12 +DA:295,0 +DA:296,0 +DA:298,12 +DA:299,1 +DA:302,1 +DA:305,11 +DA:308,11 +DA:309,4 +DA:314,7 +DA:315,1 +DA:321,6 +DA:327,11 +DA:328,1 +DA:331,1 +DA:335,10 +DA:346,10 +DA:355,10 +DA:356,10 +DA:370,9 +DA:371,9 +DA:374,9 +DA:377,1 +DA:378,1 +DA:382,1 +DA:383,1 +DA:388,12 +DA:389,12 +DA:390,12 +DA:391,12 +LF:122 +LH:116 +BRDA:71,0,0,1 +BRDA:71,0,1,7 +BRDA:88,1,0,4 +BRDA:88,1,1,3 +BRDA:102,2,0,1 +BRDA:102,2,1,1 +BRDA:122,3,0,1 +BRDA:122,3,1,0 +BRDA:127,4,0,1 +BRDA:127,4,1,2 +BRDA:133,5,0,1 +BRDA:133,5,1,1 +BRDA:152,6,0,1 +BRDA:152,6,1,0 +BRDA:157,7,0,1 +BRDA:157,7,1,1 +BRDA:171,8,0,1 +BRDA:171,8,1,1 +BRDA:192,9,0,1 +BRDA:192,9,1,0 +BRDA:202,10,0,1 +BRDA:202,10,1,2 +BRDA:208,11,0,1 +BRDA:208,11,1,1 +BRDA:230,12,0,1 +BRDA:230,12,1,0 +BRDA:235,13,0,1 +BRDA:235,13,1,1 +BRDA:246,14,0,0 +BRDA:246,14,1,1 +BRDA:268,15,0,1 +BRDA:268,15,1,0 +BRDA:290,16,0,0 +BRDA:290,16,1,12 +BRDA:294,17,0,0 +BRDA:294,17,1,12 +BRDA:298,18,0,1 +BRDA:298,18,1,11 +BRDA:308,19,0,4 +BRDA:308,19,1,7 +BRDA:314,20,0,1 +BRDA:314,20,1,6 +BRDA:327,21,0,1 +BRDA:327,21,1,10 +BRDA:335,22,0,10 +BRDA:335,22,1,9 +BRDA:338,23,0,4 +BRDA:338,23,1,5 +BRDA:339,24,0,1 +BRDA:339,24,1,8 +BRDA:377,25,0,1 +BRDA:377,25,1,0 +BRDA:380,26,0,1 +BRDA:380,26,1,0 +BRDA:388,27,0,12 +BRDA:388,27,1,1 +BRDA:389,28,0,12 +BRDA:389,28,1,0 +BRDA:390,29,0,0 +BRDA:390,29,1,12 +BRDA:44,30,0,0 +BRDA:44,30,1,4 +BRDA:44,31,0,4 +BRDA:44,31,1,0 +BRDA:44,32,0,0 +BRDA:44,32,1,4 +BRDA:44,33,0,4 +BRDA:44,33,1,0 +BRDA:46,34,0,0 +BRDA:46,34,1,4 +BRDA:46,35,0,4 +BRDA:46,35,1,0 +BRDA:48,36,0,0 +BRDA:48,36,1,4 +BRDA:48,37,0,4 +BRDA:48,37,1,0 +BRDA:50,38,0,0 +BRDA:50,38,1,4 +BRDA:50,39,0,4 +BRDA:50,39,1,0 +BRDA:52,40,0,0 +BRDA:52,40,1,4 +BRDA:52,41,0,4 +BRDA:52,41,1,0 +BRDA:54,42,0,0 +BRDA:54,42,1,4 +BRDA:54,43,0,4 +BRDA:54,43,1,0 +BRDA:56,44,0,4 +BRDA:56,44,1,0 +BRDA:56,45,0,4 +BRDA:56,45,1,4 +BRDA:57,46,0,4 +BRDA:57,46,1,0 +BRDA:57,47,0,4 +BRDA:57,47,1,4 +BRF:96 +BRH:68 +end_of_record +TN: +SF:src/contexts/organisation/application/ports/position-hierarchy.repository.port.ts +FNF:0 +FNH:0 +DA:3,2 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/organisation/application/ports/position.repository.port.ts +FNF:0 +FNH:0 +DA:7,24 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/organisation/application/ports/team.repository.port.ts +FNF:0 +FNH:0 +DA:5,7 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/organisation/application/services/position-hierarchy.service.ts +FN:10,(anonymous_4) +FN:16,(anonymous_5) +FN:32,(anonymous_6) +FN:41,(anonymous_7) +FN:48,(anonymous_8) +FN:54,(anonymous_9) +FN:62,(anonymous_10) +FNF:7 +FNH:7 +FNDA:5,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:1,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:1,(anonymous_9) +FNDA:2,(anonymous_10) +DA:1,2 +DA:5,2 +DA:6,2 +DA:9,2 +DA:12,5 +DA:13,5 +DA:20,2 +DA:21,1 +DA:26,1 +DA:27,1 +DA:29,1 +DA:36,1 +DA:37,1 +DA:38,1 +DA:44,1 +DA:46,1 +DA:47,1 +DA:49,2 +DA:57,1 +DA:58,1 +DA:61,1 +DA:63,2 +LF:22 +LH:22 +BRDA:20,0,0,1 +BRDA:20,0,1,1 +BRDA:13,1,0,2 +BRDA:13,1,1,0 +BRDA:13,2,0,2 +BRDA:13,2,1,2 +BRF:6 +BRH:5 +end_of_record +TN: +SF:src/contexts/organisation/application/services/position.service.ts +FN:22,(anonymous_4) +FN:29,(anonymous_5) +FN:37,(anonymous_6) +FN:41,(anonymous_7) +FN:47,(anonymous_8) +FN:61,(anonymous_9) +FN:78,(anonymous_10) +FN:82,(anonymous_11) +FNF:8 +FNH:8 +FNDA:11,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:10,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:3,(anonymous_10) +FNDA:3,(anonymous_11) +DA:6,19 +DA:12,19 +DA:16,19 +DA:18,19 +DA:21,19 +DA:24,11 +DA:26,11 +DA:30,2 +DA:34,2 +DA:38,1 +DA:42,10 +DA:43,10 +DA:44,7 +DA:51,2 +DA:52,1 +DA:58,1 +DA:62,3 +DA:63,2 +DA:64,2 +DA:65,1 +DA:75,1 +DA:79,3 +DA:81,3 +DA:82,3 +LF:24 +LH:24 +BRDA:32,0,0,2 +BRDA:32,0,1,1 +BRDA:43,1,0,3 +BRDA:43,1,1,7 +BRDA:54,2,0,1 +BRDA:54,2,1,0 +BRDA:64,3,0,1 +BRDA:64,3,1,1 +BRDA:26,4,0,0 +BRDA:26,4,1,19 +BRDA:26,5,0,19 +BRDA:26,5,1,0 +BRF:12 +BRH:9 +end_of_record +TN: +SF:src/contexts/organisation/application/services/team.service.ts +FN:21,(anonymous_4) +FN:27,(anonymous_5) +FN:40,(anonymous_6) +FN:44,(anonymous_7) +FN:50,(anonymous_8) +FN:67,(anonymous_9) +FN:82,(anonymous_10) +FN:101,(anonymous_11) +FN:106,(anonymous_12) +FN:116,(anonymous_13) +FNF:10 +FNH:10 +FNDA:19,(anonymous_4) +FNDA:3,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:17,(anonymous_7) +FNDA:4,(anonymous_8) +FNDA:3,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:1,(anonymous_11) +FNDA:5,(anonymous_12) +FNDA:3,(anonymous_13) +DA:7,2 +DA:13,2 +DA:15,2 +DA:17,2 +DA:20,2 +DA:23,19 +DA:24,19 +DA:28,3 +DA:29,1 +DA:32,3 +DA:37,3 +DA:41,1 +DA:45,17 +DA:46,17 +DA:47,14 +DA:51,4 +DA:53,3 +DA:54,1 +DA:57,3 +DA:64,3 +DA:68,3 +DA:69,2 +DA:70,2 +DA:71,1 +DA:79,1 +DA:87,2 +DA:88,2 +DA:90,2 +DA:95,2 +DA:96,1 +DA:98,1 +DA:102,1 +DA:103,1 +DA:110,5 +DA:111,5 +DA:113,5 +DA:115,2 +DA:117,3 +DA:118,3 +DA:121,2 +DA:123,1 +DA:128,2 +LF:42 +LH:42 +BRDA:28,0,0,1 +BRDA:28,0,1,2 +BRDA:28,1,0,3 +BRDA:28,1,1,2 +BRDA:34,2,0,3 +BRDA:34,2,1,3 +BRDA:35,3,0,3 +BRDA:35,3,1,2 +BRDA:46,4,0,3 +BRDA:46,4,1,14 +BRDA:53,5,0,1 +BRDA:53,5,1,2 +BRDA:53,6,0,3 +BRDA:53,6,1,2 +BRDA:59,7,0,1 +BRDA:59,7,1,2 +BRDA:70,8,0,1 +BRDA:70,8,1,1 +BRDA:95,9,0,1 +BRDA:95,9,1,1 +BRDA:113,10,0,3 +BRDA:113,10,1,2 +BRDA:24,11,0,2 +BRDA:24,11,1,0 +BRDA:24,12,0,2 +BRDA:24,12,1,2 +BRF:26 +BRH:25 +end_of_record +TN: +SF:src/contexts/reporting/application/listeners/cycle-stage.listener.ts +FN:21,(anonymous_4) +FN:36,(anonymous_5) +FN:47,(anonymous_6) +FNF:3 +FNH:2 +FNDA:3,(anonymous_4) +FNDA:3,(anonymous_5) +FNDA:0,(anonymous_6) +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:10,1 +DA:18,1 +DA:19,3 +DA:22,3 +DA:23,3 +DA:25,3 +DA:36,1 +DA:39,3 +DA:40,1 +DA:43,2 +DA:47,2 +DA:48,0 +DA:49,0 +DA:50,0 +DA:56,2 +DA:60,2 +DA:61,2 +DA:67,2 +DA:71,1 +DA:75,1 +DA:80,1 +LF:27 +LH:24 +BRDA:39,0,0,1 +BRDA:39,0,1,2 +BRDA:49,1,0,0 +BRDA:49,1,1,0 +BRDA:37,2,0,1 +BRDA:37,2,1,0 +BRDA:37,3,0,1 +BRDA:37,3,1,1 +BRDA:38,4,0,1 +BRDA:38,4,1,0 +BRDA:38,5,0,1 +BRDA:38,5,1,1 +BRDA:22,6,0,1 +BRDA:22,6,1,0 +BRDA:22,7,0,1 +BRDA:22,7,1,1 +BRDA:22,8,0,1 +BRDA:22,8,1,0 +BRDA:22,9,0,1 +BRDA:22,9,1,1 +BRDA:23,10,0,0 +BRDA:23,10,1,1 +BRDA:23,11,0,1 +BRDA:23,11,1,0 +BRF:24 +BRH:16 +end_of_record +TN: +SF:src/contexts/reporting/application/listeners/review-stage.listener.ts +FN:17,(anonymous_2) +FN:30,(anonymous_3) +FNF:2 +FNH:2 +FNDA:3,(anonymous_2) +FNDA:3,(anonymous_3) +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:14,1 +DA:15,3 +DA:18,3 +DA:19,3 +DA:30,1 +DA:33,3 +DA:34,1 +DA:37,2 +DA:40,2 +DA:44,2 +DA:45,2 +DA:50,2 +DA:54,1 +DA:58,1 +DA:63,1 +LF:21 +LH:21 +BRDA:33,0,0,1 +BRDA:33,0,1,2 +BRDA:31,1,0,1 +BRDA:31,1,1,0 +BRDA:31,2,0,1 +BRDA:31,2,1,1 +BRDA:32,3,0,1 +BRDA:32,3,1,0 +BRDA:32,4,0,1 +BRDA:32,4,1,1 +BRDA:18,5,0,1 +BRDA:18,5,1,0 +BRDA:18,6,0,1 +BRDA:18,6,1,1 +BRDA:18,7,0,1 +BRDA:18,7,1,0 +BRDA:18,8,0,1 +BRDA:18,8,1,1 +BRF:18 +BRH:14 +end_of_record +TN: +SF:src/contexts/reporting/application/ports/report-analytics.repository.port.ts +FNF:0 +FNH:0 +DA:3,11 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/reporting/application/ports/report-comment.repository.port.ts +FNF:0 +FNH:0 +DA:3,6 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/reporting/application/ports/report-insight.repository.port.ts +FNF:0 +FNH:0 +DA:3,6 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/reporting/application/ports/report.repository.port.ts +FNF:0 +FNH:0 +DA:4,11 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/reporting/application/ports/strategic-report-analytics.repository.port.ts +FNF:0 +FNH:0 +DA:3,5 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/reporting/application/ports/strategic-report-insight.repository.port.ts +FNF:0 +FNH:0 +DA:3,5 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/reporting/application/ports/strategic-report.repository.port.ts +FNF:0 +FNH:0 +DA:4,22 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/contexts/reporting/application/services/report-analytics.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:35,(anonymous_6) +FNF:3 +FNH:3 +FNDA:4,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:2,(anonymous_6) +DA:1,2 +DA:4,2 +DA:8,2 +DA:11,2 +DA:14,4 +DA:15,4 +DA:22,2 +DA:24,2 +DA:26,2 +DA:27,1 +DA:32,1 +DA:39,2 +DA:41,2 +DA:42,1 +DA:47,1 +DA:49,1 +LF:16 +LH:16 +BRDA:26,0,0,1 +BRDA:26,0,1,1 +BRDA:41,1,0,1 +BRDA:41,1,1,1 +BRDA:14,2,0,0 +BRDA:14,2,1,2 +BRDA:14,3,0,2 +BRDA:14,3,1,0 +BRDA:14,4,0,2 +BRDA:14,4,1,0 +BRDA:14,5,0,2 +BRDA:14,5,1,2 +BRF:12 +BRH:9 +end_of_record +TN: +SF:src/contexts/reporting/application/services/report-comment.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:35,(anonymous_6) +FN:47,(anonymous_7) +FNF:4 +FNH:4 +FNDA:6,(anonymous_4) +FNDA:3,(anonymous_5) +FNDA:2,(anonymous_6) +FNDA:1,(anonymous_7) +DA:1,2 +DA:4,2 +DA:8,2 +DA:11,2 +DA:14,6 +DA:15,6 +DA:22,3 +DA:24,2 +DA:26,2 +DA:27,1 +DA:32,1 +DA:36,2 +DA:38,2 +DA:39,1 +DA:44,1 +DA:48,1 +LF:16 +LH:16 +BRDA:26,0,0,1 +BRDA:26,0,1,1 +BRDA:26,1,0,2 +BRDA:26,1,1,2 +BRDA:38,2,0,1 +BRDA:38,2,1,1 +BRDA:14,3,0,0 +BRDA:14,3,1,2 +BRDA:14,4,0,2 +BRDA:14,4,1,0 +BRDA:14,5,0,2 +BRDA:14,5,1,0 +BRDA:14,6,0,2 +BRDA:14,6,1,2 +BRF:14 +BRH:11 +end_of_record +TN: +SF:src/contexts/reporting/application/services/report-insight.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:35,(anonymous_6) +FNF:3 +FNH:3 +FNDA:4,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:2,(anonymous_6) +DA:1,2 +DA:4,2 +DA:8,2 +DA:11,2 +DA:14,4 +DA:15,4 +DA:22,2 +DA:24,2 +DA:26,2 +DA:27,1 +DA:32,1 +DA:39,2 +DA:41,2 +DA:42,1 +DA:47,1 +DA:49,1 +LF:16 +LH:16 +BRDA:26,0,0,1 +BRDA:26,0,1,1 +BRDA:41,1,0,1 +BRDA:41,1,1,1 +BRDA:14,2,0,0 +BRDA:14,2,1,2 +BRDA:14,3,0,2 +BRDA:14,3,1,0 +BRDA:14,4,0,2 +BRDA:14,4,1,0 +BRDA:14,5,0,2 +BRDA:14,5,1,2 +BRF:12 +BRH:9 +end_of_record +TN: +SF:src/contexts/reporting/application/services/reports.service.ts +FN:87,(anonymous_5) +FN:116,(anonymous_6) +FN:128,(anonymous_7) +FN:142,(anonymous_8) +FN:159,(anonymous_9) +FN:194,(anonymous_10) +FN:216,(anonymous_11) +FN:238,(anonymous_12) +FN:240,(anonymous_13) +FN:250,(anonymous_14) +FN:259,(anonymous_15) +FN:387,(anonymous_16) +FN:418,(anonymous_17) +FN:431,(anonymous_18) +FN:445,(anonymous_19) +FN:528,(anonymous_20) +FN:541,(anonymous_21) +FN:568,(anonymous_22) +FN:572,(anonymous_23) +FN:573,(anonymous_24) +FN:576,(anonymous_25) +FN:587,(anonymous_26) +FN:604,(anonymous_27) +FN:610,(anonymous_28) +FN:635,(anonymous_29) +FN:646,(anonymous_30) +FN:766,(anonymous_31) +FN:769,(anonymous_32) +FN:772,(anonymous_33) +FN:857,(anonymous_34) +FN:864,(anonymous_35) +FN:867,(anonymous_36) +FN:887,(anonymous_37) +FN:893,(anonymous_38) +FN:898,(anonymous_39) +FN:909,(anonymous_40) +FN:925,(anonymous_41) +FN:930,(anonymous_42) +FN:933,(anonymous_43) +FN:936,(anonymous_44) +FN:981,(anonymous_45) +FN:996,(anonymous_46) +FN:1009,(anonymous_47) +FN:1062,(anonymous_48) +FN:1068,(anonymous_49) +FN:1079,(anonymous_50) +FN:1107,(anonymous_51) +FN:1108,(anonymous_52) +FN:1110,(anonymous_53) +FN:1111,(anonymous_54) +FN:1125,(anonymous_55) +FN:1131,(anonymous_56) +FN:1137,(anonymous_57) +FN:1143,(anonymous_58) +FN:1271,(anonymous_59) +FN:1273,(anonymous_60) +FN:1284,(anonymous_61) +FNF:57 +FNH:52 +FNDA:15,(anonymous_5) +FNDA:3,(anonymous_6) +FNDA:3,(anonymous_7) +FNDA:6,(anonymous_8) +FNDA:6,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:4,(anonymous_11) +FNDA:1,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:2,(anonymous_14) +FNDA:1,(anonymous_15) +FNDA:1,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:1,(anonymous_19) +FNDA:1,(anonymous_20) +FNDA:1,(anonymous_21) +FNDA:16,(anonymous_22) +FNDA:13,(anonymous_23) +FNDA:11,(anonymous_24) +FNDA:11,(anonymous_25) +FNDA:48,(anonymous_26) +FNDA:2,(anonymous_27) +FNDA:2,(anonymous_28) +FNDA:1,(anonymous_29) +FNDA:1,(anonymous_30) +FNDA:1,(anonymous_31) +FNDA:1,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:1,(anonymous_34) +FNDA:1,(anonymous_35) +FNDA:1,(anonymous_36) +FNDA:3,(anonymous_37) +FNDA:6,(anonymous_38) +FNDA:2,(anonymous_39) +FNDA:10,(anonymous_40) +FNDA:1,(anonymous_41) +FNDA:1,(anonymous_42) +FNDA:1,(anonymous_43) +FNDA:1,(anonymous_44) +FNDA:15,(anonymous_45) +FNDA:24,(anonymous_46) +FNDA:2,(anonymous_47) +FNDA:2,(anonymous_48) +FNDA:0,(anonymous_49) +FNDA:2,(anonymous_50) +FNDA:2,(anonymous_51) +FNDA:2,(anonymous_52) +FNDA:2,(anonymous_53) +FNDA:2,(anonymous_54) +FNDA:2,(anonymous_55) +FNDA:2,(anonymous_56) +FNDA:2,(anonymous_57) +FNDA:2,(anonymous_58) +FNDA:6,(anonymous_59) +FNDA:12,(anonymous_60) +FNDA:6,(anonymous_61) +DA:1,6 +DA:16,6 +DA:23,6 +DA:24,6 +DA:28,6 +DA:33,6 +DA:37,6 +DA:41,6 +DA:45,6 +DA:49,6 +DA:54,6 +DA:58,6 +DA:63,6 +DA:64,6 +DA:65,6 +DA:66,6 +DA:70,6 +DA:74,6 +DA:78,6 +DA:84,6 +DA:85,15 +DA:89,15 +DA:91,15 +DA:93,15 +DA:95,15 +DA:97,15 +DA:99,15 +DA:101,15 +DA:103,15 +DA:105,15 +DA:107,15 +DA:109,15 +DA:111,15 +DA:114,15 +DA:120,3 +DA:121,2 +DA:129,3 +DA:132,2 +DA:135,2 +DA:136,1 +DA:143,6 +DA:145,6 +DA:146,1 +DA:149,5 +DA:151,3 +DA:163,6 +DA:166,5 +DA:169,5 +DA:171,4 +DA:173,4 +DA:174,1 +DA:178,3 +DA:179,3 +DA:181,3 +DA:183,2 +DA:187,2 +DA:189,1 +DA:198,2 +DA:199,2 +DA:200,1 +DA:205,1 +DA:207,1 +DA:217,4 +DA:219,4 +DA:220,4 +DA:221,1 +DA:222,1 +DA:225,3 +DA:226,3 +DA:227,1 +DA:230,2 +DA:234,2 +DA:235,2 +DA:238,1 +DA:241,0 +DA:247,2 +DA:249,2 +DA:250,2 +DA:251,2 +DA:254,2 +DA:255,1 +DA:258,1 +DA:260,1 +DA:261,1 +DA:266,1 +DA:271,1 +DA:277,1 +DA:281,1 +DA:286,1 +DA:291,1 +DA:293,1 +DA:295,1 +DA:300,1 +DA:329,1 +DA:331,1 +DA:336,1 +DA:343,1 +DA:347,1 +DA:353,1 +DA:359,1 +DA:364,1 +DA:368,1 +DA:372,1 +DA:376,1 +DA:396,1 +DA:397,1 +DA:398,1 +DA:400,0 +DA:406,1 +DA:408,1 +DA:413,1 +DA:418,0 +DA:419,0 +DA:422,0 +DA:425,0 +DA:431,0 +DA:432,0 +DA:451,1 +DA:452,1 +DA:453,1 +DA:458,1 +DA:465,1 +DA:466,1 +DA:467,2 +DA:470,1 +DA:471,1 +DA:472,1 +DA:473,1 +DA:477,0 +DA:478,1 +DA:479,1 +DA:482,1 +DA:483,1 +DA:484,1 +DA:486,1 +DA:490,1 +DA:491,1 +DA:493,1 +DA:494,1 +DA:496,1 +DA:500,1 +DA:501,0 +DA:504,0 +DA:507,1 +DA:509,1 +DA:532,1 +DA:545,1 +DA:546,1 +DA:547,1 +DA:548,1 +DA:551,1 +DA:552,1 +DA:555,1 +DA:556,1 +DA:559,0 +DA:571,16 +DA:572,13 +DA:573,11 +DA:574,16 +DA:575,11 +DA:576,11 +DA:579,11 +DA:590,48 +DA:592,42 +DA:593,42 +DA:609,2 +DA:610,2 +DA:613,2 +DA:614,1 +DA:617,1 +DA:618,0 +DA:621,1 +DA:625,1 +DA:645,1 +DA:646,1 +DA:649,1 +DA:653,1 +DA:654,1 +DA:655,1 +DA:656,1 +DA:657,1 +DA:659,1 +DA:661,1 +DA:663,1 +DA:667,1 +DA:671,1 +DA:676,1 +DA:677,1 +DA:679,1 +DA:680,1 +DA:682,1 +DA:683,0 +DA:686,1 +DA:690,1 +DA:694,1 +DA:699,1 +DA:703,1 +DA:708,1 +DA:733,1 +DA:734,1 +DA:735,1 +DA:745,1 +DA:746,1 +DA:748,1 +DA:749,1 +DA:751,1 +DA:752,0 +DA:755,1 +DA:759,1 +DA:760,1 +DA:761,1 +DA:762,1 +DA:764,1 +DA:765,1 +DA:766,1 +DA:768,1 +DA:769,1 +DA:771,1 +DA:772,0 +DA:775,1 +DA:776,1 +DA:778,1 +DA:779,1 +DA:781,1 +DA:782,0 +DA:785,1 +DA:789,1 +DA:793,1 +DA:798,1 +DA:802,1 +DA:807,1 +DA:833,1 +DA:861,1 +DA:865,1 +DA:867,1 +DA:869,1 +DA:870,1 +DA:871,2 +DA:872,0 +DA:874,2 +DA:875,2 +DA:876,2 +DA:878,1 +DA:891,3 +DA:894,6 +DA:898,2 +DA:899,3 +DA:913,10 +DA:914,5 +DA:916,5 +DA:929,1 +DA:930,1 +DA:932,1 +DA:933,1 +DA:935,1 +DA:936,1 +DA:939,1 +DA:943,1 +DA:947,1 +DA:952,1 +DA:956,1 +DA:960,1 +DA:985,15 +DA:986,5 +DA:988,10 +DA:997,24 +DA:998,15 +DA:1015,2 +DA:1016,2 +DA:1017,2 +DA:1019,2 +DA:1023,2 +DA:1027,2 +DA:1032,2 +DA:1036,2 +DA:1041,2 +DA:1067,2 +DA:1070,0 +DA:1074,0 +DA:1077,0 +DA:1080,2 +DA:1106,2 +DA:1107,2 +DA:1108,2 +DA:1109,2 +DA:1110,2 +DA:1111,2 +DA:1114,2 +DA:1116,2 +DA:1118,2 +DA:1120,2 +DA:1122,2 +DA:1126,2 +DA:1132,2 +DA:1138,2 +DA:1144,2 +DA:1149,2 +DA:1151,2 +DA:1179,2 +DA:1207,2 +DA:1235,2 +DA:1263,2 +DA:1271,15 +DA:1272,6 +DA:1273,12 +DA:1275,6 +DA:1276,6 +DA:1285,6 +DA:1286,6 +DA:1287,6 +DA:1289,6 +LF:311 +LH:290 +BRDA:129,0,0,1 +BRDA:129,0,1,2 +BRDA:132,1,0,2 +BRDA:132,1,1,2 +BRDA:135,2,0,1 +BRDA:135,2,1,1 +BRDA:145,3,0,1 +BRDA:145,3,1,5 +BRDA:163,4,0,1 +BRDA:163,4,1,5 +BRDA:166,5,0,5 +BRDA:166,5,1,5 +BRDA:169,6,0,1 +BRDA:169,6,1,4 +BRDA:173,7,0,1 +BRDA:173,7,1,3 +BRDA:181,8,0,1 +BRDA:181,8,1,2 +BRDA:181,9,0,3 +BRDA:181,9,1,3 +BRDA:187,10,0,1 +BRDA:187,10,1,1 +BRDA:199,11,0,1 +BRDA:199,11,1,1 +BRDA:220,12,0,1 +BRDA:220,12,1,3 +BRDA:226,13,0,1 +BRDA:226,13,1,2 +BRDA:254,14,0,1 +BRDA:254,14,1,1 +BRDA:268,15,0,1 +BRDA:268,15,1,0 +BRDA:273,16,0,1 +BRDA:273,16,1,1 +BRDA:396,17,0,1 +BRDA:396,17,1,0 +BRDA:408,18,0,1 +BRDA:408,18,1,0 +BRDA:409,19,0,1 +BRDA:409,19,1,1 +BRDA:419,20,0,0 +BRDA:419,20,1,0 +BRDA:431,21,0,0 +BRDA:431,21,1,0 +BRDA:453,22,0,1 +BRDA:453,22,1,0 +BRDA:454,23,0,1 +BRDA:454,23,1,1 +BRDA:454,23,2,1 +BRDA:467,24,0,1 +BRDA:467,24,1,1 +BRDA:472,25,0,0 +BRDA:472,25,1,1 +BRDA:473,26,0,0 +BRDA:473,26,1,1 +BRDA:474,27,0,1 +BRDA:474,27,1,1 +BRDA:478,28,0,1 +BRDA:478,28,1,1 +BRDA:484,29,0,0 +BRDA:484,29,1,1 +BRDA:484,30,0,1 +BRDA:484,30,1,1 +BRDA:487,31,0,1 +BRDA:487,31,1,0 +BRDA:488,32,0,1 +BRDA:488,32,1,1 +BRDA:491,33,0,0 +BRDA:491,33,1,1 +BRDA:494,34,0,0 +BRDA:494,34,1,1 +BRDA:500,35,0,0 +BRDA:500,35,1,1 +BRDA:507,36,0,1 +BRDA:507,36,1,0 +BRDA:511,37,0,1 +BRDA:511,37,1,0 +BRDA:552,38,0,1 +BRDA:552,38,1,0 +BRDA:555,39,0,1 +BRDA:555,39,1,0 +BRDA:555,40,0,1 +BRDA:555,40,1,1 +BRDA:572,41,0,13 +BRDA:572,41,1,11 +BRDA:573,42,0,11 +BRDA:573,42,1,0 +BRDA:574,43,0,5 +BRDA:574,43,1,11 +BRDA:590,44,0,6 +BRDA:590,44,1,42 +BRDA:590,45,0,48 +BRDA:590,45,1,42 +BRDA:592,46,0,42 +BRDA:592,46,1,0 +BRDA:613,47,0,1 +BRDA:613,47,1,1 +BRDA:617,48,0,0 +BRDA:617,48,1,1 +BRDA:661,49,0,1 +BRDA:661,49,1,0 +BRDA:676,50,0,1 +BRDA:676,50,1,0 +BRDA:679,51,0,1 +BRDA:679,51,1,0 +BRDA:682,52,0,0 +BRDA:682,52,1,1 +BRDA:717,53,0,1 +BRDA:717,53,1,0 +BRDA:718,54,0,1 +BRDA:718,54,1,0 +BRDA:719,55,0,1 +BRDA:719,55,1,1 +BRDA:721,56,0,1 +BRDA:721,56,1,0 +BRDA:723,57,0,1 +BRDA:723,57,1,0 +BRDA:725,58,0,1 +BRDA:725,58,1,1 +BRDA:727,59,0,1 +BRDA:727,59,1,0 +BRDA:729,60,0,1 +BRDA:729,60,1,1 +BRDA:734,61,0,1 +BRDA:734,61,1,0 +BRDA:734,62,0,1 +BRDA:734,62,1,1 +BRDA:735,63,0,1 +BRDA:735,63,1,1 +BRDA:739,64,0,1 +BRDA:739,64,1,0 +BRDA:745,65,0,1 +BRDA:745,65,1,0 +BRDA:748,66,0,1 +BRDA:748,66,1,0 +BRDA:751,67,0,0 +BRDA:751,67,1,1 +BRDA:775,68,0,1 +BRDA:775,68,1,0 +BRDA:778,69,0,1 +BRDA:778,69,1,0 +BRDA:781,70,0,0 +BRDA:781,70,1,1 +BRDA:816,71,0,1 +BRDA:816,71,1,0 +BRDA:817,72,0,1 +BRDA:817,72,1,0 +BRDA:818,73,0,1 +BRDA:818,73,1,1 +BRDA:820,74,0,1 +BRDA:820,74,1,0 +BRDA:822,75,0,1 +BRDA:822,75,1,0 +BRDA:824,76,0,1 +BRDA:824,76,1,1 +BRDA:826,77,0,1 +BRDA:826,77,1,0 +BRDA:828,78,0,1 +BRDA:828,78,1,1 +BRDA:871,79,0,0 +BRDA:871,79,1,2 +BRDA:874,80,0,2 +BRDA:874,80,1,1 +BRDA:894,81,0,6 +BRDA:894,81,1,2 +BRDA:894,81,2,2 +BRDA:913,82,0,5 +BRDA:913,82,1,5 +BRDA:913,83,0,10 +BRDA:913,83,1,5 +BRDA:985,84,0,5 +BRDA:985,84,1,10 +BRDA:985,85,0,15 +BRDA:985,85,1,10 +BRDA:997,86,0,9 +BRDA:997,86,1,15 +BRDA:1070,87,0,0 +BRDA:1070,87,1,0 +BRDA:1071,88,0,0 +BRDA:1071,88,1,0 +BRDA:1072,89,0,0 +BRDA:1072,89,1,0 +BRDA:1074,90,0,0 +BRDA:1074,90,1,0 +BRDA:1075,91,0,0 +BRDA:1075,91,1,0 +BRDA:1076,92,0,0 +BRDA:1076,92,1,0 +BRDA:1082,93,0,1 +BRDA:1082,93,1,1 +BRDA:1087,94,0,2 +BRDA:1087,94,1,0 +BRDA:1087,95,0,1 +BRDA:1087,95,1,1 +BRDA:1108,96,0,2 +BRDA:1108,96,1,2 +BRDA:1111,97,0,2 +BRDA:1111,97,1,2 +BRDA:1114,98,0,2 +BRDA:1114,98,1,0 +BRDA:1116,99,0,2 +BRDA:1116,99,1,0 +BRDA:1118,100,0,2 +BRDA:1118,100,1,0 +BRDA:1120,101,0,2 +BRDA:1120,101,1,0 +BRDA:1124,102,0,2 +BRDA:1124,102,1,0 +BRDA:1126,103,0,2 +BRDA:1126,103,1,0 +BRDA:1130,104,0,2 +BRDA:1130,104,1,0 +BRDA:1132,105,0,2 +BRDA:1132,105,1,0 +BRDA:1136,106,0,2 +BRDA:1136,106,1,0 +BRDA:1138,107,0,2 +BRDA:1138,107,1,0 +BRDA:1142,108,0,2 +BRDA:1142,108,1,0 +BRDA:1144,109,0,2 +BRDA:1144,109,1,0 +BRDA:1157,110,0,1 +BRDA:1157,110,1,1 +BRDA:1158,111,0,1 +BRDA:1158,111,1,0 +BRDA:1161,112,0,1 +BRDA:1161,112,1,1 +BRDA:1162,113,0,1 +BRDA:1162,113,1,0 +BRDA:1165,114,0,1 +BRDA:1165,114,1,1 +BRDA:1166,115,0,1 +BRDA:1166,115,1,0 +BRDA:1169,116,0,1 +BRDA:1169,116,1,1 +BRDA:1170,117,0,1 +BRDA:1170,117,1,0 +BRDA:1185,118,0,1 +BRDA:1185,118,1,1 +BRDA:1186,119,0,1 +BRDA:1186,119,1,0 +BRDA:1189,120,0,1 +BRDA:1189,120,1,1 +BRDA:1190,121,0,1 +BRDA:1190,121,1,0 +BRDA:1193,122,0,1 +BRDA:1193,122,1,1 +BRDA:1194,123,0,1 +BRDA:1194,123,1,0 +BRDA:1197,124,0,1 +BRDA:1197,124,1,1 +BRDA:1198,125,0,1 +BRDA:1198,125,1,0 +BRDA:1213,126,0,1 +BRDA:1213,126,1,1 +BRDA:1214,127,0,1 +BRDA:1214,127,1,0 +BRDA:1217,128,0,1 +BRDA:1217,128,1,1 +BRDA:1218,129,0,1 +BRDA:1218,129,1,0 +BRDA:1221,130,0,1 +BRDA:1221,130,1,1 +BRDA:1222,131,0,1 +BRDA:1222,131,1,0 +BRDA:1225,132,0,1 +BRDA:1225,132,1,1 +BRDA:1226,133,0,1 +BRDA:1226,133,1,0 +BRDA:1241,134,0,1 +BRDA:1241,134,1,1 +BRDA:1242,135,0,1 +BRDA:1242,135,1,0 +BRDA:1245,136,0,1 +BRDA:1245,136,1,1 +BRDA:1246,137,0,1 +BRDA:1246,137,1,0 +BRDA:1249,138,0,1 +BRDA:1249,138,1,1 +BRDA:1250,139,0,1 +BRDA:1250,139,1,0 +BRDA:1253,140,0,1 +BRDA:1253,140,1,1 +BRDA:1254,141,0,1 +BRDA:1254,141,1,0 +BRDA:1273,142,0,12 +BRDA:1273,142,1,6 +BRDA:1275,143,0,0 +BRDA:1275,143,1,6 +BRDA:89,144,0,0 +BRDA:89,144,1,6 +BRDA:89,145,0,6 +BRDA:89,145,1,0 +BRDA:89,146,0,0 +BRDA:89,146,1,6 +BRDA:89,147,0,6 +BRDA:89,147,1,0 +BRDA:91,148,0,0 +BRDA:91,148,1,6 +BRDA:91,149,0,6 +BRDA:91,149,1,0 +BRDA:93,150,0,0 +BRDA:93,150,1,6 +BRDA:93,151,0,6 +BRDA:93,151,1,0 +BRDA:95,152,0,0 +BRDA:95,152,1,6 +BRDA:95,153,0,6 +BRDA:95,153,1,0 +BRDA:97,154,0,0 +BRDA:97,154,1,6 +BRDA:97,155,0,6 +BRDA:97,155,1,0 +BRDA:99,156,0,0 +BRDA:99,156,1,6 +BRDA:99,157,0,6 +BRDA:99,157,1,0 +BRDA:101,158,0,0 +BRDA:101,158,1,6 +BRDA:101,159,0,6 +BRDA:101,159,1,0 +BRDA:103,160,0,0 +BRDA:103,160,1,6 +BRDA:103,161,0,6 +BRDA:103,161,1,0 +BRDA:105,162,0,0 +BRDA:105,162,1,6 +BRDA:105,163,0,6 +BRDA:105,163,1,0 +BRDA:107,164,0,0 +BRDA:107,164,1,6 +BRDA:107,165,0,6 +BRDA:107,165,1,0 +BRDA:109,166,0,0 +BRDA:109,166,1,6 +BRDA:109,167,0,6 +BRDA:109,167,1,0 +BRF:338 +BRH:227 +end_of_record +TN: +SF:src/contexts/reporting/application/services/startegic-report-insight.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:36,(anonymous_6) +FNF:3 +FNH:3 +FNDA:4,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:2,(anonymous_6) +DA:1,2 +DA:4,2 +DA:8,2 +DA:11,2 +DA:14,4 +DA:15,4 +DA:22,2 +DA:25,2 +DA:27,2 +DA:28,1 +DA:33,1 +DA:40,2 +DA:42,2 +DA:43,1 +DA:48,1 +DA:50,1 +LF:16 +LH:16 +BRDA:27,0,0,1 +BRDA:27,0,1,1 +BRDA:42,1,0,1 +BRDA:42,1,1,1 +BRDA:14,2,0,0 +BRDA:14,2,1,2 +BRDA:14,3,0,2 +BRDA:14,3,1,0 +BRDA:14,4,0,2 +BRDA:14,4,1,0 +BRDA:14,5,0,2 +BRDA:14,5,1,2 +BRF:12 +BRH:9 +end_of_record +TN: +SF:src/contexts/reporting/application/services/strategic-report-analytics.service.ts +FN:12,(anonymous_4) +FN:18,(anonymous_5) +FN:38,(anonymous_6) +FNF:3 +FNH:3 +FNDA:4,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:2,(anonymous_6) +DA:1,2 +DA:4,2 +DA:8,2 +DA:11,2 +DA:14,4 +DA:15,4 +DA:22,2 +DA:25,2 +DA:29,2 +DA:30,1 +DA:35,1 +DA:42,2 +DA:44,2 +DA:45,1 +DA:50,1 +DA:55,1 +LF:16 +LH:16 +BRDA:29,0,0,1 +BRDA:29,0,1,1 +BRDA:44,1,0,1 +BRDA:44,1,1,1 +BRDA:14,2,0,0 +BRDA:14,2,1,2 +BRDA:14,3,0,2 +BRDA:14,3,1,0 +BRDA:14,4,0,2 +BRDA:14,4,1,0 +BRDA:14,5,0,2 +BRDA:14,5,1,2 +BRF:12 +BRH:9 +end_of_record +TN: +SF:src/contexts/reporting/application/services/strategic-reports.service.ts +FN:98,(anonymous_5) +FN:131,(anonymous_6) +FN:143,(anonymous_7) +FN:157,(anonymous_8) +FN:179,(anonymous_9) +FN:199,(anonymous_10) +FN:209,(anonymous_11) +FN:230,(anonymous_12) +FN:319,(anonymous_13) +FN:349,(anonymous_14) +FN:362,(anonymous_15) +FN:375,(anonymous_16) +FN:382,(anonymous_17) +FN:414,(anonymous_18) +FN:418,(anonymous_19) +FN:441,(anonymous_20) +FN:442,(anonymous_21) +FN:444,(anonymous_22) +FN:445,(anonymous_23) +FN:447,(anonymous_24) +FN:451,(anonymous_25) +FN:452,(anonymous_26) +FN:453,(anonymous_27) +FN:455,(anonymous_28) +FN:458,(anonymous_29) +FN:461,(anonymous_30) +FN:465,(anonymous_31) +FN:468,(anonymous_32) +FN:471,(anonymous_33) +FN:553,(anonymous_34) +FN:557,(anonymous_35) +FN:558,(anonymous_36) +FN:561,(anonymous_37) +FN:572,(anonymous_38) +FN:588,(anonymous_39) +FN:609,(anonymous_40) +FN:621,(anonymous_41) +FN:622,(anonymous_42) +FN:630,(anonymous_43) +FN:633,(anonymous_44) +FN:636,(anonymous_45) +FN:693,(anonymous_46) +FN:698,(anonymous_47) +FN:701,(anonymous_48) +FN:704,(anonymous_49) +FN:749,(anonymous_50) +FN:766,(anonymous_51) +FN:782,(anonymous_52) +FN:794,(anonymous_53) +FN:799,(anonymous_54) +FN:804,(anonymous_55) +FN:828,(anonymous_56) +FN:829,(anonymous_57) +FN:831,(anonymous_58) +FN:832,(anonymous_59) +FN:846,(anonymous_60) +FN:852,(anonymous_61) +FN:858,(anonymous_62) +FN:864,(anonymous_63) +FN:936,(anonymous_64) +FN:938,(anonymous_65) +FN:949,(anonymous_66) +FNF:62 +FNH:55 +FNDA:16,(anonymous_5) +FNDA:3,(anonymous_6) +FNDA:3,(anonymous_7) +FNDA:5,(anonymous_8) +FNDA:5,(anonymous_9) +FNDA:2,(anonymous_10) +FNDA:2,(anonymous_11) +FNDA:6,(anonymous_12) +FNDA:2,(anonymous_13) +FNDA:2,(anonymous_14) +FNDA:3,(anonymous_15) +FNDA:1,(anonymous_16) +FNDA:2,(anonymous_17) +FNDA:1,(anonymous_18) +FNDA:3,(anonymous_19) +FNDA:2,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:2,(anonymous_22) +FNDA:1,(anonymous_23) +FNDA:9,(anonymous_24) +FNDA:1,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:1,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:3,(anonymous_31) +FNDA:3,(anonymous_32) +FNDA:3,(anonymous_33) +FNDA:12,(anonymous_34) +FNDA:6,(anonymous_35) +FNDA:6,(anonymous_36) +FNDA:6,(anonymous_37) +FNDA:26,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:3,(anonymous_40) +FNDA:1,(anonymous_41) +FNDA:1,(anonymous_42) +FNDA:1,(anonymous_43) +FNDA:1,(anonymous_44) +FNDA:1,(anonymous_45) +FNDA:3,(anonymous_46) +FNDA:1,(anonymous_47) +FNDA:1,(anonymous_48) +FNDA:1,(anonymous_49) +FNDA:12,(anonymous_50) +FNDA:8,(anonymous_51) +FNDA:24,(anonymous_52) +FNDA:3,(anonymous_53) +FNDA:0,(anonymous_54) +FNDA:1,(anonymous_55) +FNDA:1,(anonymous_56) +FNDA:1,(anonymous_57) +FNDA:1,(anonymous_58) +FNDA:1,(anonymous_59) +FNDA:1,(anonymous_60) +FNDA:1,(anonymous_61) +FNDA:1,(anonymous_62) +FNDA:1,(anonymous_63) +FNDA:3,(anonymous_64) +FNDA:6,(anonymous_65) +FNDA:3,(anonymous_66) +DA:1,5 +DA:12,5 +DA:19,5 +DA:20,5 +DA:24,5 +DA:28,5 +DA:34,5 +DA:39,5 +DA:44,5 +DA:48,5 +DA:54,5 +DA:58,5 +DA:62,5 +DA:70,5 +DA:71,5 +DA:72,5 +DA:73,5 +DA:77,5 +DA:81,5 +DA:85,5 +DA:89,5 +DA:95,5 +DA:96,16 +DA:100,16 +DA:102,16 +DA:104,16 +DA:106,16 +DA:108,16 +DA:110,16 +DA:112,16 +DA:114,16 +DA:116,16 +DA:118,16 +DA:120,16 +DA:122,16 +DA:124,16 +DA:126,16 +DA:128,16 +DA:135,3 +DA:136,2 +DA:144,3 +DA:147,2 +DA:150,2 +DA:151,1 +DA:161,5 +DA:163,5 +DA:164,1 +DA:169,4 +DA:171,2 +DA:183,5 +DA:186,4 +DA:189,4 +DA:191,3 +DA:193,3 +DA:194,1 +DA:198,2 +DA:199,2 +DA:202,2 +DA:204,1 +DA:213,2 +DA:214,2 +DA:215,1 +DA:220,1 +DA:222,1 +DA:233,6 +DA:235,6 +DA:236,6 +DA:237,1 +DA:240,5 +DA:241,5 +DA:242,1 +DA:243,1 +DA:246,4 +DA:247,4 +DA:248,1 +DA:253,3 +DA:254,3 +DA:261,3 +DA:263,3 +DA:264,3 +DA:265,3 +DA:266,2 +DA:267,2 +DA:270,3 +DA:282,3 +DA:283,3 +DA:284,3 +DA:285,3 +DA:290,3 +DA:291,3 +DA:292,3 +DA:293,3 +DA:294,3 +DA:296,3 +DA:297,2 +DA:298,2 +DA:299,2 +DA:300,2 +DA:302,2 +DA:303,2 +DA:304,2 +DA:306,2 +DA:307,1 +DA:310,2 +DA:311,1 +DA:315,2 +DA:319,2 +DA:320,2 +DA:322,2 +DA:323,2 +DA:327,2 +DA:328,2 +DA:329,2 +DA:330,2 +DA:331,2 +DA:336,2 +DA:337,1 +DA:338,1 +DA:339,1 +DA:340,1 +DA:345,2 +DA:349,2 +DA:350,2 +DA:352,2 +DA:353,2 +DA:358,2 +DA:362,2 +DA:363,3 +DA:367,3 +DA:374,2 +DA:376,1 +DA:380,2 +DA:381,2 +DA:382,2 +DA:384,2 +DA:385,2 +DA:390,2 +DA:391,1 +DA:392,1 +DA:393,1 +DA:394,1 +DA:397,1 +DA:398,1 +DA:399,1 +DA:403,2 +DA:404,1 +DA:406,1 +DA:407,1 +DA:408,1 +DA:413,2 +DA:414,2 +DA:417,3 +DA:419,3 +DA:429,3 +DA:430,3 +DA:435,3 +DA:440,3 +DA:441,2 +DA:442,0 +DA:443,3 +DA:444,2 +DA:445,1 +DA:446,3 +DA:447,3 +DA:448,9 +DA:450,3 +DA:451,1 +DA:452,0 +DA:453,3 +DA:454,3 +DA:455,0 +DA:457,3 +DA:458,0 +DA:460,3 +DA:461,0 +DA:464,3 +DA:465,3 +DA:467,3 +DA:468,3 +DA:470,3 +DA:471,3 +DA:474,3 +DA:508,3 +DA:510,3 +DA:514,3 +DA:518,3 +DA:523,3 +DA:527,3 +DA:531,3 +DA:534,3 +DA:535,1 +DA:540,2 +DA:544,2 +DA:556,12 +DA:557,6 +DA:558,6 +DA:559,12 +DA:560,6 +DA:561,6 +DA:564,6 +DA:575,26 +DA:577,26 +DA:578,26 +DA:592,0 +DA:593,0 +DA:596,0 +DA:598,0 +DA:617,3 +DA:619,3 +DA:620,1 +DA:621,1 +DA:622,1 +DA:623,1 +DA:624,1 +DA:629,1 +DA:630,1 +DA:632,1 +DA:633,1 +DA:635,1 +DA:636,1 +DA:639,1 +DA:643,1 +DA:647,1 +DA:652,1 +DA:656,1 +DA:661,1 +DA:684,3 +DA:697,3 +DA:698,1 +DA:700,3 +DA:701,1 +DA:703,3 +DA:704,1 +DA:707,3 +DA:711,3 +DA:715,3 +DA:720,3 +DA:724,3 +DA:728,3 +DA:753,12 +DA:754,6 +DA:756,6 +DA:770,8 +DA:771,4 +DA:774,4 +DA:783,24 +DA:784,8 +DA:798,3 +DA:800,0 +DA:801,0 +DA:802,0 +DA:805,1 +DA:827,3 +DA:828,1 +DA:829,1 +DA:830,3 +DA:831,1 +DA:832,1 +DA:835,3 +DA:837,3 +DA:839,3 +DA:841,3 +DA:843,3 +DA:847,1 +DA:853,1 +DA:859,1 +DA:865,1 +DA:870,3 +DA:872,3 +DA:886,3 +DA:900,3 +DA:914,3 +DA:928,3 +DA:936,16 +DA:937,3 +DA:938,6 +DA:940,3 +DA:941,3 +DA:950,3 +DA:951,3 +DA:952,6 +DA:954,3 +LF:282 +LH:270 +BRDA:144,0,0,1 +BRDA:144,0,1,2 +BRDA:147,1,0,2 +BRDA:147,1,1,2 +BRDA:150,2,0,1 +BRDA:150,2,1,1 +BRDA:163,3,0,1 +BRDA:163,3,1,4 +BRDA:183,4,0,1 +BRDA:183,4,1,4 +BRDA:186,5,0,4 +BRDA:186,5,1,4 +BRDA:189,6,0,1 +BRDA:189,6,1,3 +BRDA:193,7,0,1 +BRDA:193,7,1,2 +BRDA:193,8,0,3 +BRDA:193,8,1,3 +BRDA:202,9,0,1 +BRDA:202,9,1,1 +BRDA:214,10,0,1 +BRDA:214,10,1,1 +BRDA:236,11,0,1 +BRDA:236,11,1,5 +BRDA:241,12,0,1 +BRDA:241,12,1,4 +BRDA:247,13,0,1 +BRDA:247,13,1,3 +BRDA:247,14,0,4 +BRDA:247,14,1,4 +BRDA:265,15,0,2 +BRDA:265,15,1,1 +BRDA:273,16,0,1 +BRDA:273,16,1,2 +BRDA:276,17,0,1 +BRDA:276,17,1,2 +BRDA:303,18,0,2 +BRDA:303,18,1,0 +BRDA:306,19,0,1 +BRDA:306,19,1,1 +BRDA:310,20,0,1 +BRDA:310,20,1,1 +BRDA:322,21,0,2 +BRDA:322,21,1,0 +BRDA:328,22,0,2 +BRDA:328,22,1,0 +BRDA:330,23,0,2 +BRDA:330,23,1,0 +BRDA:337,24,0,1 +BRDA:337,24,1,0 +BRDA:339,25,0,1 +BRDA:339,25,1,0 +BRDA:352,26,0,2 +BRDA:352,26,1,0 +BRDA:363,27,0,3 +BRDA:363,27,1,0 +BRDA:380,28,0,2 +BRDA:380,28,1,1 +BRDA:384,29,0,2 +BRDA:384,29,1,0 +BRDA:385,30,0,1 +BRDA:385,30,1,1 +BRDA:391,31,0,1 +BRDA:391,31,1,0 +BRDA:393,32,0,1 +BRDA:393,32,1,0 +BRDA:398,33,0,1 +BRDA:398,33,1,0 +BRDA:398,34,0,1 +BRDA:398,34,1,1 +BRDA:404,35,0,1 +BRDA:404,35,1,0 +BRDA:407,36,0,1 +BRDA:407,36,1,0 +BRDA:534,37,0,1 +BRDA:534,37,1,2 +BRDA:557,38,0,6 +BRDA:557,38,1,6 +BRDA:558,39,0,6 +BRDA:558,39,1,0 +BRDA:559,40,0,6 +BRDA:559,40,1,6 +BRDA:575,41,0,0 +BRDA:575,41,1,26 +BRDA:575,42,0,26 +BRDA:575,42,1,26 +BRDA:577,43,0,26 +BRDA:577,43,1,0 +BRDA:592,44,0,0 +BRDA:592,44,1,0 +BRDA:598,45,0,0 +BRDA:598,45,1,0 +BRDA:623,46,0,1 +BRDA:623,46,1,0 +BRDA:667,47,0,1 +BRDA:667,47,1,0 +BRDA:668,48,0,1 +BRDA:668,48,1,0 +BRDA:669,49,0,1 +BRDA:669,49,1,0 +BRDA:671,50,0,1 +BRDA:671,50,1,0 +BRDA:673,51,0,1 +BRDA:673,51,1,0 +BRDA:675,52,0,1 +BRDA:675,52,1,0 +BRDA:677,53,0,1 +BRDA:677,53,1,0 +BRDA:679,54,0,1 +BRDA:679,54,1,0 +BRDA:753,55,0,6 +BRDA:753,55,1,6 +BRDA:753,56,0,12 +BRDA:753,56,1,6 +BRDA:770,57,0,4 +BRDA:770,57,1,4 +BRDA:770,58,0,8 +BRDA:770,58,1,4 +BRDA:783,59,0,16 +BRDA:783,59,1,8 +BRDA:800,60,0,0 +BRDA:800,60,1,0 +BRDA:801,61,0,0 +BRDA:801,61,1,0 +BRDA:829,62,0,1 +BRDA:829,62,1,1 +BRDA:832,63,0,1 +BRDA:832,63,1,1 +BRDA:835,64,0,1 +BRDA:835,64,1,2 +BRDA:837,65,0,1 +BRDA:837,65,1,2 +BRDA:839,66,0,1 +BRDA:839,66,1,2 +BRDA:841,67,0,1 +BRDA:841,67,1,2 +BRDA:845,68,0,1 +BRDA:845,68,1,2 +BRDA:847,69,0,1 +BRDA:847,69,1,0 +BRDA:851,70,0,1 +BRDA:851,70,1,2 +BRDA:853,71,0,1 +BRDA:853,71,1,0 +BRDA:857,72,0,1 +BRDA:857,72,1,2 +BRDA:859,73,0,1 +BRDA:859,73,1,0 +BRDA:863,74,0,1 +BRDA:863,74,1,2 +BRDA:865,75,0,1 +BRDA:865,75,1,0 +BRDA:876,76,0,3 +BRDA:876,76,1,2 +BRDA:878,77,0,3 +BRDA:878,77,1,2 +BRDA:890,78,0,3 +BRDA:890,78,1,2 +BRDA:892,79,0,3 +BRDA:892,79,1,2 +BRDA:904,80,0,3 +BRDA:904,80,1,2 +BRDA:906,81,0,3 +BRDA:906,81,1,2 +BRDA:918,82,0,3 +BRDA:918,82,1,2 +BRDA:920,83,0,3 +BRDA:920,83,1,2 +BRDA:938,84,0,6 +BRDA:938,84,1,6 +BRDA:940,85,0,0 +BRDA:940,85,1,3 +BRDA:100,86,0,0 +BRDA:100,86,1,5 +BRDA:100,87,0,5 +BRDA:100,87,1,0 +BRDA:100,88,0,0 +BRDA:100,88,1,5 +BRDA:100,89,0,5 +BRDA:100,89,1,0 +BRDA:102,90,0,0 +BRDA:102,90,1,5 +BRDA:102,91,0,5 +BRDA:102,91,1,0 +BRDA:104,92,0,0 +BRDA:104,92,1,5 +BRDA:104,93,0,5 +BRDA:104,93,1,0 +BRDA:106,94,0,0 +BRDA:106,94,1,5 +BRDA:106,95,0,5 +BRDA:106,95,1,0 +BRDA:108,96,0,0 +BRDA:108,96,1,5 +BRDA:108,97,0,5 +BRDA:108,97,1,0 +BRDA:110,98,0,0 +BRDA:110,98,1,5 +BRDA:110,99,0,5 +BRDA:110,99,1,0 +BRDA:112,100,0,0 +BRDA:112,100,1,5 +BRDA:112,101,0,5 +BRDA:112,101,1,0 +BRDA:114,102,0,0 +BRDA:114,102,1,5 +BRDA:114,103,0,5 +BRDA:114,103,1,0 +BRDA:116,104,0,0 +BRDA:116,104,1,5 +BRDA:116,105,0,5 +BRDA:116,105,1,0 +BRDA:118,106,0,0 +BRDA:118,106,1,5 +BRDA:118,107,0,5 +BRDA:118,107,1,0 +BRDA:120,108,0,0 +BRDA:120,108,1,5 +BRDA:120,109,0,5 +BRDA:120,109,1,0 +BRDA:122,110,0,0 +BRDA:122,110,1,5 +BRDA:122,111,0,5 +BRDA:122,111,1,0 +BRDA:124,112,0,0 +BRDA:124,112,1,5 +BRDA:124,113,0,5 +BRDA:124,113,1,0 +BRDA:126,114,0,0 +BRDA:126,114,1,5 +BRDA:126,115,0,5 +BRDA:126,115,1,0 +BRF:232 +BRH:163 +end_of_record +TN: +SF:src/contexts/reporting/application/services/text-answer.service.ts +FN:20,(anonymous_4) +FN:27,(anonymous_5) +FN:42,(anonymous_6) +FN:48,(anonymous_7) +FN:50,(anonymous_8) +FN:66,(anonymous_9) +FNF:6 +FNH:6 +FNDA:4,(anonymous_4) +FNDA:4,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:4,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:4,(anonymous_9) +DA:1,2 +DA:6,2 +DA:8,2 +DA:12,2 +DA:19,2 +DA:22,4 +DA:24,4 +DA:31,4 +DA:33,3 +DA:41,3 +DA:42,3 +DA:43,1 +DA:46,3 +DA:48,4 +DA:51,2 +DA:52,2 +DA:67,4 +DA:70,3 +DA:73,3 +DA:75,1 +LF:20 +LH:20 +BRDA:48,0,0,4 +BRDA:48,0,1,3 +BRDA:54,1,0,2 +BRDA:54,1,1,1 +BRDA:67,2,0,1 +BRDA:67,2,1,3 +BRDA:70,3,0,3 +BRDA:70,3,1,2 +BRDA:73,4,0,2 +BRDA:73,4,1,1 +BRDA:22,5,0,0 +BRDA:22,5,1,2 +BRDA:22,6,0,2 +BRDA:22,6,1,0 +BRDA:22,7,0,0 +BRDA:22,7,1,2 +BRDA:22,8,0,2 +BRDA:22,8,1,0 +BRF:18 +BRH:14 +end_of_record diff --git a/apps/api/test/unit/dashboard.html b/apps/api/test/unit/dashboard.html new file mode 100644 index 00000000..27fe1681 --- /dev/null +++ b/apps/api/test/unit/dashboard.html @@ -0,0 +1,429 @@ + + + + + + Unit Tests Dashboard + + + + + +
+

+ Unit Tests Dashboard + PASS +

+
+ Unit suite · згенеровано 19 травня 2026 р. о 00:52 +
+
+ +
+
+
Test suites
+
140
+
+ 140 passed +
+
+
+
Tests
+
752
+
+ 752 passed +
+
+
+
Pass rate
+
100.0%
+
752/752 tests
+
+
+
Duration
+
20.89 s
+
cumulative across suites
+
+
+ +
+
+

Tests per context

+
+
+
+

Distribution by context

+
+
+
+ +
+
+

Suites per context

+
+
+
+

Duration per context

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ContextSuitesTestsPassedFailedDuration
+ identity + 10494901.21 s
+ organisation + 17868601.95 s
+ library + 2110610602.42 s
+ feedback360 + 4930130108.46 s
+ reporting + 3316416405.33 s
+ notifications + 10464601.53 s
+ +
+ Згенеровано із Jest AggregatedResult · + 140 spec files +
+ + + + \ No newline at end of file diff --git a/apps/api/test/unit/jest.unit.config.ts b/apps/api/test/unit/jest.unit.config.ts new file mode 100644 index 00000000..e1f262c3 --- /dev/null +++ b/apps/api/test/unit/jest.unit.config.ts @@ -0,0 +1,46 @@ +import type { Config } from 'jest'; + +/** + * Jest configuration for unit tests. + * + * Lives next to the unit test files so it travels with them. + * `rootDir` points back at `apps/api/` so `/src/...` mappings + * keep working as if Jest were invoked from there. Used via + * `pnpm test:unit` / `pnpm test:unit:cov` from `apps/api/`. + * + * The `testRegex` matches `*.spec.ts` but NOT `*.integration-spec.ts`, + * so integration tests stay in their own suite. + */ +const config: Config = { + moduleFileExtensions: ['js', 'json', 'ts'], + // The config file is at apps/api/test/unit/, so two levels up + // lands at apps/api/ — the directory that holds src/. + rootDir: '../..', + moduleNameMapper: { + '^src/(.*)$': '/src/$1', + }, + // Match `*.spec.ts` only; `\.spec\.ts$` requires a literal dot before + // `spec`, so `foo.integration-spec.ts` (which ends in `-spec.ts`) is + // intentionally excluded. + testRegex: '\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + testEnvironment: 'node', + verbose: false, + // Silence NestJS Logger output (see setup-jest.ts for the reasoning). + // `setupFilesAfterEnv` runs AFTER Jest's test environment is installed, + // so `jest.spyOn` etc. are available inside setup-jest.ts. + setupFilesAfterEnv: ['/test/unit/setup-jest.ts'], + // Coverage (enabled via `--coverage` / `pnpm test:unit:cov`). + // Scope: application services / listeners — the layers unit tests + // actually exercise. The integration-test config covers infrastructure + // from a different angle, so the two reports complement each other. + coverageDirectory: 'test/unit/coverage', + collectCoverageFrom: [ + 'src/contexts/*/application/**/*.ts', + '!src/contexts/*/application/**/*.spec.ts', + ], +}; + +export default config; diff --git a/apps/api/test/unit/results.json b/apps/api/test/unit/results.json new file mode 100644 index 00000000..d5660118 --- /dev/null +++ b/apps/api/test/unit/results.json @@ -0,0 +1,18263 @@ +{ + "numFailedTestSuites": 0, + "numFailedTests": 0, + "numPassedTestSuites": 140, + "numPassedTests": 752, + "numPendingTestSuites": 0, + "numPendingTests": 0, + "numRuntimeErrorTestSuites": 0, + "numTodoTests": 0, + "numTotalTestSuites": 140, + "numTotalTests": 752, + "openHandles": [], + "snapshot": { + "added": 0, + "didUpdate": false, + "failure": false, + "filesAdded": 0, + "filesRemoved": 0, + "filesRemovedList": [], + "filesUnmatched": 0, + "filesUpdated": 0, + "matched": 0, + "total": 0, + "unchecked": 0, + "uncheckedKeysByFile": [], + "unmatched": 0, + "updated": 0 + }, + "startTime": 1779141132332, + "success": false, + "testResults": [ + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 16, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133372, + "loadTestEnvironmentEnd": 1779141132781, + "loadTestEnvironmentStart": 1779141132551, + "runtime": 590, + "setupAfterEnvEnd": 1779141133142, + "setupAfterEnvStart": 1779141132947, + "setupFilesEnd": 1779141132782, + "setupFilesStart": 1779141132782, + "slow": false, + "start": 1779141132782 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/services/strategic-reports.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportingService", + "search" + ], + "duration": 11, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService search delegates to the repository when no actor is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133323, + "status": "passed", + "title": "delegates to the repository when no actor is supplied" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "search" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService search throws ForbiddenException for non-admin/HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133334, + "status": "passed", + "title": "throws ForbiddenException for non-admin/HR actors" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "search" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService search returns reports for admin/HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133338, + "status": "passed", + "title": "returns reports for admin/HR actors" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService getById throws NotFoundException when the report is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133341, + "status": "passed", + "title": "throws NotFoundException when the report is missing" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService getById returns the report for HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133343, + "status": "passed", + "title": "returns the report for HR actors" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "getById" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService getById throws NotFoundException when the cycle has no reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133345, + "status": "passed", + "title": "throws NotFoundException when the cycle has no reviews" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService getById returns the report when actor manages at least one review in the cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133349, + "status": "passed", + "title": "returns the report when actor manages at least one review in the cycle" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService getById throws ForbiddenException when actor has no relation to the cycle reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133351, + "status": "passed", + "title": "throws ForbiddenException when actor has no relation to the cycle reviews" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "getByCycleId" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService getByCycleId throws NotFoundException when no report exists for the cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133352, + "status": "passed", + "title": "throws NotFoundException when no report exists for the cycle" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "getByCycleId" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService getByCycleId returns the report after checking access", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133354, + "status": "passed", + "title": "returns the report after checking access" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "generateStrategicReportForCycle" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService generateStrategicReportForCycle throws NotFoundException when the cycle does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133355, + "status": "passed", + "title": "throws NotFoundException when the cycle does not exist" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "generateStrategicReportForCycle" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService generateStrategicReportForCycle returns the existing strategic report when one already exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133356, + "status": "passed", + "title": "returns the existing strategic report when one already exists" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "generateStrategicReportForCycle" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService generateStrategicReportForCycle throws NotFoundException when there are no per-review reports", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133358, + "status": "passed", + "title": "throws NotFoundException when there are no per-review reports" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "generateStrategicReportForCycle" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService generateStrategicReportForCycle throws when the freshly created report cannot be re-loaded", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133359, + "status": "passed", + "title": "throws when the freshly created report cannot be re-loaded" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "generateStrategicReportForCycle" + ], + "duration": 7, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService generateStrategicReportForCycle aggregates reviews, ratees, respondents, answers, reviewers, and competences into the report", + "invocations": 1, + "location": null, + "numPassingAsserts": 17, + "retryReasons": [], + "startAt": 1779141133362, + "status": "passed", + "title": "aggregates reviews, ratees, respondents, answers, reviewers, and competences into the report" + }, + { + "ancestorTitles": [ + "StrategicReportingService", + "generateStrategicReportForCycle" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingService generateStrategicReportForCycle keeps rateeTurnout at 0 when no self-assessment answer is found for the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133369, + "status": "passed", + "title": "keeps rateeTurnout at 0 when no self-assessment answer is found for the review" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 21, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133395, + "loadTestEnvironmentEnd": 1779141132770, + "loadTestEnvironmentStart": 1779141132542, + "runtime": 625, + "setupAfterEnvEnd": 1779141133136, + "setupAfterEnvStart": 1779141132960, + "setupFilesEnd": 1779141132770, + "setupFilesStart": 1779141132770, + "slow": false, + "start": 1779141132770 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/services/cycle.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleService", + "create" + ], + "duration": 8, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService create creates a cycle and then returns it via getById", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141133361, + "status": "passed", + "title": "creates a cycle and then returns it via getById" + }, + { + "ancestorTitles": [ + "CycleService", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService create uses payload values for stage, isActive and minRespondentsThreshold when provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133369, + "status": "passed", + "title": "uses payload values for stage, isActive and minRespondentsThreshold when provided" + }, + { + "ancestorTitles": [ + "CycleService", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService search delegates to the cycle repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133370, + "status": "passed", + "title": "delegates to the cycle repository" + }, + { + "ancestorTitles": [ + "CycleService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService getById returns the cycle when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133371, + "status": "passed", + "title": "returns the cycle when found" + }, + { + "ancestorTitles": [ + "CycleService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133372, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "CycleService", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService update blocks title changes when a strategic report exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133373, + "status": "passed", + "title": "blocks title changes when a strategic report exists" + }, + { + "ancestorTitles": [ + "CycleService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService update forwards only defined patch fields to updateById", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133373, + "status": "passed", + "title": "forwards only defined patch fields to updateById" + }, + { + "ancestorTitles": [ + "CycleService", + "update" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService update blocks stage transition to FINISHED when reviews are still active", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133374, + "status": "passed", + "title": "blocks stage transition to FINISHED when reviews are still active" + }, + { + "ancestorTitles": [ + "CycleService", + "update" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService update runs completeCycle when patching to FINISHED with no active reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133376, + "status": "passed", + "title": "runs completeCycle when patching to FINISHED with no active reviews" + }, + { + "ancestorTitles": [ + "CycleService", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService update skips changeStage when patch.stage equals current stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133380, + "status": "passed", + "title": "skips changeStage when patch.stage equals current stage" + }, + { + "ancestorTitles": [ + "CycleService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService update does not call changeStage when target stage is PUBLISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133380, + "status": "passed", + "title": "does not call changeStage when target stage is PUBLISHED" + }, + { + "ancestorTitles": [ + "CycleService", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService delete throws NotFoundException when cycle is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133381, + "status": "passed", + "title": "throws NotFoundException when cycle is missing" + }, + { + "ancestorTitles": [ + "CycleService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService delete deletes the cycle when it exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133381, + "status": "passed", + "title": "deletes the cycle when it exists" + }, + { + "ancestorTitles": [ + "CycleService", + "changeStage" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService changeStage throws on an invalid transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133382, + "status": "passed", + "title": "throws on an invalid transition" + }, + { + "ancestorTitles": [ + "CycleService", + "changeStage" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService changeStage updates the stage, writes a history record, and emits the event", + "invocations": 1, + "location": null, + "numPassingAsserts": 10, + "retryReasons": [], + "startAt": 1779141133382, + "status": "passed", + "title": "updates the stage, writes a history record, and emits the event" + }, + { + "ancestorTitles": [ + "CycleService", + "changeStage" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService changeStage uses SYSTEM_ACTOR defaults when actor information is omitted", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133383, + "status": "passed", + "title": "uses SYSTEM_ACTOR defaults when actor information is omitted" + }, + { + "ancestorTitles": [ + "CycleService", + "completeCycle" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService completeCycle moves the cycle to FINISHED when all reviews are non-active", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133384, + "status": "passed", + "title": "moves the cycle to FINISHED when all reviews are non-active" + }, + { + "ancestorTitles": [ + "CycleService", + "completeCycle" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService completeCycle throws when at least one review is still IN_PROGRESS", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133385, + "status": "passed", + "title": "throws when at least one review is still IN_PROGRESS" + }, + { + "ancestorTitles": [ + "CycleService", + "completeCycle" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService completeCycle does nothing when there are no reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133387, + "status": "passed", + "title": "does nothing when there are no reviews" + }, + { + "ancestorTitles": [ + "CycleService", + "forceFinish" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService forceFinish transitions the cycle to FINISHED with the FORCE_FINISH reason", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141133389, + "status": "passed", + "title": "transitions the cycle to FINISHED with the FORCE_FINISH reason" + }, + { + "ancestorTitles": [ + "CycleService", + "getStageHistory" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleService getStageHistory delegates to the stageHistory repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133392, + "status": "passed", + "title": "delegates to the stageHistory repository" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 15, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133396, + "loadTestEnvironmentEnd": 1779141132796, + "loadTestEnvironmentStart": 1779141132540, + "runtime": 599, + "setupAfterEnvEnd": 1779141133202, + "setupAfterEnvStart": 1779141132995, + "setupFilesEnd": 1779141132797, + "setupFilesStart": 1779141132797, + "slow": false, + "start": 1779141132797 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/services/reports.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportingService", + "search" + ], + "duration": 10, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService search delegates to the repository when no actor is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133347, + "status": "passed", + "title": "delegates to the repository when no actor is supplied" + }, + { + "ancestorTitles": [ + "ReportingService", + "search" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService search throws ForbiddenException for non-admin/HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133357, + "status": "passed", + "title": "throws ForbiddenException for non-admin/HR actors" + }, + { + "ancestorTitles": [ + "ReportingService", + "search" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService search allows admin/HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133361, + "status": "passed", + "title": "allows admin/HR actors" + }, + { + "ancestorTitles": [ + "ReportingService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService getById throws NotFoundException when the report is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133364, + "status": "passed", + "title": "throws NotFoundException when the report is missing" + }, + { + "ancestorTitles": [ + "ReportingService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService getById returns the report for HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133366, + "status": "passed", + "title": "returns the report for HR actors" + }, + { + "ancestorTitles": [ + "ReportingService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService getById returns the report when the actor is the ratee", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133367, + "status": "passed", + "title": "returns the report when the actor is the ratee" + }, + { + "ancestorTitles": [ + "ReportingService", + "getById" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService getById returns the report when the actor is a reviewer", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133368, + "status": "passed", + "title": "returns the report when the actor is a reviewer" + }, + { + "ancestorTitles": [ + "ReportingService", + "getById" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService getById throws ForbiddenException when the actor has no relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133371, + "status": "passed", + "title": "throws ForbiddenException when the actor has no relation" + }, + { + "ancestorTitles": [ + "ReportingService", + "getById" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService getById throws NotFoundException when the underlying review is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133374, + "status": "passed", + "title": "throws NotFoundException when the underlying review is missing" + }, + { + "ancestorTitles": [ + "ReportingService", + "getByReviewId" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService getByReviewId throws NotFoundException when no report exists for the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133377, + "status": "passed", + "title": "throws NotFoundException when no report exists for the review" + }, + { + "ancestorTitles": [ + "ReportingService", + "getByReviewId" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService getByReviewId returns the report after checking access", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133380, + "status": "passed", + "title": "returns the report after checking access" + }, + { + "ancestorTitles": [ + "ReportingService", + "generateReportForReview" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService generateReportForReview returns the existing report when one is already stored", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133383, + "status": "passed", + "title": "returns the existing report when one is already stored" + }, + { + "ancestorTitles": [ + "ReportingService", + "generateReportForReview" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService generateReportForReview throws when the underlying review is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133386, + "status": "passed", + "title": "throws when the underlying review is missing" + }, + { + "ancestorTitles": [ + "ReportingService", + "generateReportForReview" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService generateReportForReview generates a report with analytics, insights and updates the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141133387, + "status": "passed", + "title": "generates a report with analytics, insights and updates the review" + }, + { + "ancestorTitles": [ + "ReportingService", + "generateReportForReview" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingService generateReportForReview throws when only self-assessment answers exist in production mode", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133391, + "status": "passed", + "title": "throws when only self-assessment answers exist in production mode" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 18, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133415, + "loadTestEnvironmentEnd": 1779141132735, + "loadTestEnvironmentStart": 1779141132539, + "runtime": 680, + "setupAfterEnvEnd": 1779141133131, + "setupAfterEnvStart": 1779141132945, + "setupFilesEnd": 1779141132735, + "setupFilesStart": 1779141132735, + "slow": false, + "start": 1779141132735 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/services/cluster-score-analytics.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "upsert" + ], + "duration": 13, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService upsert verifies the cycle exists before validating and upserting", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141133373, + "status": "passed", + "title": "verifies the cycle exists before validating and upserting" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "upsert" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService upsert throws when min score exceeds max score", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133387, + "status": "passed", + "title": "throws when min score exceeds max score" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "upsert" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService upsert throws when the average is outside [min, max]", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133391, + "status": "passed", + "title": "throws when the average is outside [min, max]" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "upsert" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService upsert throws when the lower bound is below the score minimum", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133392, + "status": "passed", + "title": "throws when the lower bound is below the score minimum" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "upsert" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService upsert throws when the upper bound exceeds the score maximum", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133394, + "status": "passed", + "title": "throws when the upper bound exceeds the score maximum" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "upsert" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService upsert throws when the lower bound exceeds the upper bound", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133395, + "status": "passed", + "title": "throws when the lower bound exceeds the upper bound" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService search delegates to the analytics repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133396, + "status": "passed", + "title": "delegates to the analytics repository" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133397, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService getById returns the row when present", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133399, + "status": "passed", + "title": "returns the row when present" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "update" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService update validates the merged min/max/avg against existing values", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133400, + "status": "passed", + "title": "validates the merged min/max/avg against existing values" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService update forwards only defined patch fields to updateById", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133402, + "status": "passed", + "title": "forwards only defined patch fields to updateById" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService delete throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133403, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService delete deletes when present", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133404, + "status": "passed", + "title": "deletes when present" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "getByCycleId" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService getByCycleId delegates to the analytics repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133405, + "status": "passed", + "title": "delegates to the analytics repository" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "generateAnalyticsForCycle" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService generateAnalyticsForCycle computes min/max/average and employee density per cluster", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141133405, + "status": "passed", + "title": "computes min/max/average and employee density per cluster" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "generateAnalyticsForCycle" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService generateAnalyticsForCycle skips clusters with no scores", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133408, + "status": "passed", + "title": "skips clusters with no scores" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "generateAnalyticsForCycle" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService generateAnalyticsForCycle validates bounds and scores per cluster and surfaces the error", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133409, + "status": "passed", + "title": "validates bounds and scores per cluster and surfaces the error" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsService", + "generateAnalyticsForCycle" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsService generateAnalyticsForCycle rounds scores to 4 decimal places when aggregating", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133411, + "status": "passed", + "title": "rounds scores to 4 decimal places when aggregating" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 19, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133466, + "loadTestEnvironmentEnd": 1779141132743, + "loadTestEnvironmentStart": 1779141132531, + "runtime": 722, + "setupAfterEnvEnd": 1779141133123, + "setupAfterEnvStart": 1779141132943, + "setupFilesEnd": 1779141132744, + "setupFilesStart": 1779141132744, + "slow": false, + "start": 1779141132744 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/application/services/review-email-notification.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyUserWelcome" + ], + "duration": 11, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyUserWelcome returns 0 when the user does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133403, + "status": "passed", + "title": "returns 0 when the user does not exist" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyUserWelcome" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyUserWelcome creates a per-user log, sends the email, and marks it sent", + "invocations": 1, + "location": null, + "numPassingAsserts": 14, + "retryReasons": [], + "startAt": 1779141133415, + "status": "passed", + "title": "creates a per-user log, sends the email, and marks it sent" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyUserWelcome" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyUserWelcome skips dispatch when a sent log already exists for the user", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133419, + "status": "passed", + "title": "skips dispatch when a sent log already exists for the user" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyUserWelcome" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyUserWelcome reuses an existing unsent log without creating a new one", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133422, + "status": "passed", + "title": "reuses an existing unsent log without creating a new one" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyUserWelcome" + ], + "duration": 18, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyUserWelcome marks failure when mailer.sendMail throws", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133423, + "status": "passed", + "title": "marks failure when mailer.sendMail throws" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyUserWelcome" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyUserWelcome skips dispatch when the recipient has no email", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133442, + "status": "passed", + "title": "skips dispatch when the recipient has no email" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyCycleStrategicReportReady" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyCycleStrategicReportReady returns 0 when the HR user is not found", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133445, + "status": "passed", + "title": "returns 0 when the HR user is not found" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyCycleStrategicReportReady" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyCycleStrategicReportReady sends the strategic-report email to HR with the correct action URL", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141133447, + "status": "passed", + "title": "sends the strategic-report email to HR with the correct action URL" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyRateeSelfAssessment" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyRateeSelfAssessment returns 0 when the review does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133450, + "status": "passed", + "title": "returns 0 when the review does not exist" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyRateeSelfAssessment" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyRateeSelfAssessment returns 0 when the ratee user does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133451, + "status": "passed", + "title": "returns 0 when the ratee user does not exist" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyRateeSelfAssessment" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyRateeSelfAssessment sends the self-assessment email scoped to the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141133452, + "status": "passed", + "title": "sends the self-assessment email scoped to the review" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyRespondents" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyRespondents returns 0 when the review does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133454, + "status": "passed", + "title": "returns 0 when the review does not exist" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyRespondents" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyRespondents iterates respondents, skips missing users, and counts successful sends", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141133456, + "status": "passed", + "title": "iterates respondents, skips missing users, and counts successful sends" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyHrReportReady" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyHrReportReady returns 0 when review or HR is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133457, + "status": "passed", + "title": "returns 0 when review or HR is missing" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyHrReportReady" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyHrReportReady sends the HR report-ready email with the comments URL", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133459, + "status": "passed", + "title": "sends the HR report-ready email with the comments URL" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyReviewers" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyReviewers returns 0 when the review is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133460, + "status": "passed", + "title": "returns 0 when the review is missing" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "notifyReviewers" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService notifyReviewers iterates reviewers, sends a per-reviewer email, and counts successful sends", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141133460, + "status": "passed", + "title": "iterates reviewers, sends a per-reviewer email, and counts successful sends" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "URL building" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService URL building strips a trailing slash from the frontend base and prepends one to the path", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133461, + "status": "passed", + "title": "strips a trailing slash from the frontend base and prepends one to the path" + }, + { + "ancestorTitles": [ + "ReviewEmailNotificationService", + "URL building" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewEmailNotificationService URL building falls back to \"/\" when no frontend link is configured", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133463, + "status": "passed", + "title": "falls back to \"/\" when no frontend link is configured" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133481, + "loadTestEnvironmentEnd": 1779141132812, + "loadTestEnvironmentStart": 1779141132548, + "runtime": 669, + "setupAfterEnvEnd": 1779141133188, + "setupAfterEnvStart": 1779141132994, + "setupFilesEnd": 1779141132812, + "setupFilesStart": 1779141132812, + "slow": false, + "start": 1779141132812 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/listeners/respondent-status.listener.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RespondentStatusListener" + ], + "duration": 9, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener ignores transitions that are not COMPLETED or CANCELED", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133445, + "status": "passed", + "title": "ignores transitions that are not COMPLETED or CANCELED" + }, + { + "ancestorTitles": [ + "RespondentStatusListener" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener completes the review when all respondents are COMPLETED or CANCELED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133454, + "status": "passed", + "title": "completes the review when all respondents are COMPLETED or CANCELED" + }, + { + "ancestorTitles": [ + "RespondentStatusListener" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener does not complete the review if any response is still pending", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133457, + "status": "passed", + "title": "does not complete the review if any response is still pending" + }, + { + "ancestorTitles": [ + "RespondentStatusListener" + ], + "duration": 10, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentStatusListener swallows errors when checking completion fails", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133458, + "status": "passed", + "title": "swallows errors when checking completion fails" + }, + { + "ancestorTitles": [ + "SelfAssessmentCompletedListener" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "SelfAssessmentCompletedListener ignores transitions that are not COMPLETED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133468, + "status": "passed", + "title": "ignores transitions that are not COMPLETED" + }, + { + "ancestorTitles": [ + "SelfAssessmentCompletedListener" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "SelfAssessmentCompletedListener ignores respondents that are not SELF_ASSESSMENT category", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133471, + "status": "passed", + "title": "ignores respondents that are not SELF_ASSESSMENT category" + }, + { + "ancestorTitles": [ + "SelfAssessmentCompletedListener" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "SelfAssessmentCompletedListener logs a warning and returns when the review cannot be found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133473, + "status": "passed", + "title": "logs a warning and returns when the review cannot be found" + }, + { + "ancestorTitles": [ + "SelfAssessmentCompletedListener" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "SelfAssessmentCompletedListener skips when the review is not in SELF_ASSESSMENT stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133474, + "status": "passed", + "title": "skips when the review is not in SELF_ASSESSMENT stage" + }, + { + "ancestorTitles": [ + "SelfAssessmentCompletedListener" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "SelfAssessmentCompletedListener advances the review from SELF_ASSESSMENT to IN_PROGRESS on self-assessment completion", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133476, + "status": "passed", + "title": "advances the review from SELF_ASSESSMENT to IN_PROGRESS on self-assessment completion" + }, + { + "ancestorTitles": [ + "SelfAssessmentCompletedListener" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "SelfAssessmentCompletedListener swallows errors from the review stage transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133476, + "status": "passed", + "title": "swallows errors from the review stage transition" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 65, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133516, + "loadTestEnvironmentEnd": 1779141132761, + "loadTestEnvironmentStart": 1779141132528, + "runtime": 755, + "setupAfterEnvEnd": 1779141133119, + "setupAfterEnvStart": 1779141132939, + "setupFilesEnd": 1779141132761, + "setupFilesStart": 1779141132761, + "slow": false, + "start": 1779141132761 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/services/review.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewService", + "create" + ], + "duration": 8, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService create verifies the cycle exists before creating the review when cycleId is provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133373, + "status": "passed", + "title": "verifies the cycle exists before creating the review when cycleId is provided" + }, + { + "ancestorTitles": [ + "ReviewService", + "create" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService create does not call cycles.getById when no cycleId is provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133382, + "status": "passed", + "title": "does not call cycles.getById when no cycleId is provided" + }, + { + "ancestorTitles": [ + "ReviewService", + "getById" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getById throws NotFoundException when the review is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133386, + "status": "passed", + "title": "throws NotFoundException when the review is missing" + }, + { + "ancestorTitles": [ + "ReviewService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getById returns the review when no actor is provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133390, + "status": "passed", + "title": "returns the review when no actor is provided" + }, + { + "ancestorTitles": [ + "ReviewService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getById returns the review for admin/HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133392, + "status": "passed", + "title": "returns the review for admin/HR actors" + }, + { + "ancestorTitles": [ + "ReviewService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getById returns the review when actor is the ratee", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133393, + "status": "passed", + "title": "returns the review when actor is the ratee" + }, + { + "ancestorTitles": [ + "ReviewService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getById returns the review when actor is the manager", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133395, + "status": "passed", + "title": "returns the review when actor is the manager" + }, + { + "ancestorTitles": [ + "ReviewService", + "getById" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getById returns the review when actor is a respondent", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133397, + "status": "passed", + "title": "returns the review when actor is a respondent" + }, + { + "ancestorTitles": [ + "ReviewService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getById returns the review when actor is a reviewer", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133400, + "status": "passed", + "title": "returns the review when actor is a reviewer" + }, + { + "ancestorTitles": [ + "ReviewService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getById throws ForbiddenException when actor has no access", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133402, + "status": "passed", + "title": "throws ForbiddenException when actor has no access" + }, + { + "ancestorTitles": [ + "ReviewService", + "search" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService search filters out reviews the actor cannot read", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133404, + "status": "passed", + "title": "filters out reviews the actor cannot read" + }, + { + "ancestorTitles": [ + "ReviewService", + "search" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService search returns all reviews for admin/HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133406, + "status": "passed", + "title": "returns all reviews for admin/HR actors" + }, + { + "ancestorTitles": [ + "ReviewService", + "update" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService update rejects updates when the review is not NEW or PROCESSING_BY_HR", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133409, + "status": "passed", + "title": "rejects updates when the review is not NEW or PROCESSING_BY_HR" + }, + { + "ancestorTitles": [ + "ReviewService", + "update" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService update rejects reassigning to a cycle outside NEW/ACTIVE", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133412, + "status": "passed", + "title": "rejects reassigning to a cycle outside NEW/ACTIVE" + }, + { + "ancestorTitles": [ + "ReviewService", + "update" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService update allows updating an editable field and forwards only defined fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133414, + "status": "passed", + "title": "allows updating an editable field and forwards only defined fields" + }, + { + "ancestorTitles": [ + "ReviewService", + "update" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService update runs changeReviewStage on a valid non-FINISHED stage patch", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133417, + "status": "passed", + "title": "runs changeReviewStage on a valid non-FINISHED stage patch" + }, + { + "ancestorTitles": [ + "ReviewService", + "delete" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService delete throws when the review has answers already submitted", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133420, + "status": "passed", + "title": "throws when the review has answers already submitted" + }, + { + "ancestorTitles": [ + "ReviewService", + "delete" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService delete deletes when there are no answers", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133423, + "status": "passed", + "title": "deletes when there are no answers" + }, + { + "ancestorTitles": [ + "ReviewService", + "createQuestion" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService createQuestion rejects when the cycle is not in NEW/ACTIVE stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133426, + "status": "passed", + "title": "rejects when the cycle is not in NEW/ACTIVE stage" + }, + { + "ancestorTitles": [ + "ReviewService", + "createQuestion" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService createQuestion falls back to template fields when payload fields are missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141133428, + "status": "passed", + "title": "falls back to template fields when payload fields are missing" + }, + { + "ancestorTitles": [ + "ReviewService", + "createQuestion" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService createQuestion throws when neither payload nor template provide a title or answer type", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133431, + "status": "passed", + "title": "throws when neither payload nor template provide a title or answer type" + }, + { + "ancestorTitles": [ + "ReviewService", + "deleteQuestion" + ], + "duration": 9, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService deleteQuestion throws NotFoundException when the question does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133432, + "status": "passed", + "title": "throws NotFoundException when the question does not exist" + }, + { + "ancestorTitles": [ + "ReviewService", + "deleteQuestion" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService deleteQuestion rejects deletion when the cycle is not NEW/ACTIVE", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133441, + "status": "passed", + "title": "rejects deletion when the cycle is not NEW/ACTIVE" + }, + { + "ancestorTitles": [ + "ReviewService", + "deleteQuestion" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService deleteQuestion deletes when the question has no cycle attached", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133444, + "status": "passed", + "title": "deletes when the question has no cycle attached" + }, + { + "ancestorTitles": [ + "ReviewService", + "attachQuestion" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService attachQuestion rejects when the review is not in NEW stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133445, + "status": "passed", + "title": "rejects when the review is not in NEW stage" + }, + { + "ancestorTitles": [ + "ReviewService", + "attachQuestion" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService attachQuestion reuses an existing question snapshot when one already exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133446, + "status": "passed", + "title": "reuses an existing question snapshot when one already exists" + }, + { + "ancestorTitles": [ + "ReviewService", + "attachQuestion" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService attachQuestion creates a new question snapshot when none exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133448, + "status": "passed", + "title": "creates a new question snapshot when none exists" + }, + { + "ancestorTitles": [ + "ReviewService", + "detachQuestion" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService detachQuestion rejects detaching when the review has started", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133450, + "status": "passed", + "title": "rejects detaching when the review has started" + }, + { + "ancestorTitles": [ + "ReviewService", + "detachQuestion" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService detachQuestion unlinks the relation when the review is NEW", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133451, + "status": "passed", + "title": "unlinks the relation when the review is NEW" + }, + { + "ancestorTitles": [ + "ReviewService", + "listQuestionRelations" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listQuestionRelations returns the question relations after the access check passes", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133454, + "status": "passed", + "title": "returns the question relations after the access check passes" + }, + { + "ancestorTitles": [ + "ReviewService", + "listQuestions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listQuestions delegates to the questions repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133456, + "status": "passed", + "title": "delegates to the questions repository" + }, + { + "ancestorTitles": [ + "ReviewService", + "addAnswer" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService addAnswer rejects adding an answer when the review is not in progress", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133456, + "status": "passed", + "title": "rejects adding an answer when the review is not in progress" + }, + { + "ancestorTitles": [ + "ReviewService", + "addAnswer" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService addAnswer persists the answer when the review is IN_PROGRESS", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133458, + "status": "passed", + "title": "persists the answer when the review is IN_PROGRESS" + }, + { + "ancestorTitles": [ + "ReviewService", + "listAnswers" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listAnswers returns answers filtered by respondent category", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133459, + "status": "passed", + "title": "returns answers filtered by respondent category" + }, + { + "ancestorTitles": [ + "ReviewService", + "listAnswers" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listAnswers allows the manager of the review to read answers", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133461, + "status": "passed", + "title": "allows the manager of the review to read answers" + }, + { + "ancestorTitles": [ + "ReviewService", + "listAnswers" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listAnswers allows a respondent who is also a reviewer of the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133462, + "status": "passed", + "title": "allows a respondent who is also a reviewer of the review" + }, + { + "ancestorTitles": [ + "ReviewService", + "listAnswers" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listAnswers throws ForbiddenException when the actor has no relation to the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133463, + "status": "passed", + "title": "throws ForbiddenException when the actor has no relation to the review" + }, + { + "ancestorTitles": [ + "ReviewService", + "countReviewAnswers" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService countReviewAnswers sums the answers count per respondent category", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133466, + "status": "passed", + "title": "sums the answers count per respondent category" + }, + { + "ancestorTitles": [ + "ReviewService", + "countReviewAnswers" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService countReviewAnswers allows the ratee of the review to count answers", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133469, + "status": "passed", + "title": "allows the ratee of the review to count answers" + }, + { + "ancestorTitles": [ + "ReviewService", + "addRespondent" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService addRespondent rejects adding to a review in FINISHED stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133471, + "status": "passed", + "title": "rejects adding to a review in FINISHED stage" + }, + { + "ancestorTitles": [ + "ReviewService", + "addRespondent" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService addRespondent persists the respondent for an in-progress review", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133472, + "status": "passed", + "title": "persists the respondent for an in-progress review" + }, + { + "ancestorTitles": [ + "ReviewService", + "updateRespondent" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService updateRespondent forbids non-admin actors that are not the respondent themselves", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133473, + "status": "passed", + "title": "forbids non-admin actors that are not the respondent themselves" + }, + { + "ancestorTitles": [ + "ReviewService", + "updateRespondent" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService updateRespondent rejects when the underlying review is not in an in-progress stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133475, + "status": "passed", + "title": "rejects when the underlying review is not in an in-progress stage" + }, + { + "ancestorTitles": [ + "ReviewService", + "updateRespondent" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService updateRespondent updates fields and triggers a status transition when responseStatus is patched", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133476, + "status": "passed", + "title": "updates fields and triggers a status transition when responseStatus is patched" + }, + { + "ancestorTitles": [ + "ReviewService", + "changeRespondentStatus" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService changeRespondentStatus rejects when respondent does not belong to the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133477, + "status": "passed", + "title": "rejects when respondent does not belong to the review" + }, + { + "ancestorTitles": [ + "ReviewService", + "changeRespondentStatus" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService changeRespondentStatus rejects an invalid transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133477, + "status": "passed", + "title": "rejects an invalid transition" + }, + { + "ancestorTitles": [ + "ReviewService", + "changeRespondentStatus" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService changeRespondentStatus updates the status and emits the event for a valid transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141133479, + "status": "passed", + "title": "updates the status and emits the event for a valid transition" + }, + { + "ancestorTitles": [ + "ReviewService", + "listRespondents" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listRespondents delegates to the respondents repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133481, + "status": "passed", + "title": "delegates to the respondents repository" + }, + { + "ancestorTitles": [ + "ReviewService", + "removeRespondent" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService removeRespondent delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133484, + "status": "passed", + "title": "delegates to the repository" + }, + { + "ancestorTitles": [ + "ReviewService", + "addReviewer" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService addReviewer verifies the review exists before persisting", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133485, + "status": "passed", + "title": "verifies the review exists before persisting" + }, + { + "ancestorTitles": [ + "ReviewService", + "listReviewers" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listReviewers delegates to the reviewer repository when actor has access", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133486, + "status": "passed", + "title": "delegates to the reviewer repository when actor has access" + }, + { + "ancestorTitles": [ + "ReviewService", + "listReviewers" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listReviewers throws ForbiddenException when the actor has no relation to the review", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133489, + "status": "passed", + "title": "throws ForbiddenException when the actor has no relation to the review" + }, + { + "ancestorTitles": [ + "ReviewService", + "removeReviewer" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService removeReviewer delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133489, + "status": "passed", + "title": "delegates to the repository" + }, + { + "ancestorTitles": [ + "ReviewService", + "upsertClusterScore" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService upsertClusterScore verifies the cycle when cycleId is provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133490, + "status": "passed", + "title": "verifies the cycle when cycleId is provided" + }, + { + "ancestorTitles": [ + "ReviewService", + "upsertClusterScore" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService upsertClusterScore defaults answersCount to 1 when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133491, + "status": "passed", + "title": "defaults answersCount to 1 when missing" + }, + { + "ancestorTitles": [ + "ReviewService", + "listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore listClusterScores delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133493, + "status": "passed", + "title": "listClusterScores delegates to the repository" + }, + { + "ancestorTitles": [ + "ReviewService", + "listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore getClusterScoreById delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133494, + "status": "passed", + "title": "getClusterScoreById delegates to the repository" + }, + { + "ancestorTitles": [ + "ReviewService", + "listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore getClusterScoreByCycleId delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133495, + "status": "passed", + "title": "getClusterScoreByCycleId delegates to the repository" + }, + { + "ancestorTitles": [ + "ReviewService", + "listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService listClusterScores / getClusterScoreById / getClusterScoreByCycleId / removeClusterScore removeClusterScore delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133496, + "status": "passed", + "title": "removeClusterScore delegates to the repository" + }, + { + "ancestorTitles": [ + "ReviewService", + "changeReviewStage" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService changeReviewStage rejects an invalid stage transition", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133497, + "status": "passed", + "title": "rejects an invalid stage transition" + }, + { + "ancestorTitles": [ + "ReviewService", + "changeReviewStage" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService changeReviewStage updates the stage, creates a history record, and emits the event", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141133498, + "status": "passed", + "title": "updates the stage, creates a history record, and emits the event" + }, + { + "ancestorTitles": [ + "ReviewService", + "completeReview" + ], + "duration": 8, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService completeReview moves the review to FINISHED when all responses are completed/canceled", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133499, + "status": "passed", + "title": "moves the review to FINISHED when all responses are completed/canceled" + }, + { + "ancestorTitles": [ + "ReviewService", + "completeReview" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService completeReview throws when any response is still pending or in progress", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133507, + "status": "passed", + "title": "throws when any response is still pending or in progress" + }, + { + "ancestorTitles": [ + "ReviewService", + "forceFinishReview" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService forceFinishReview runs changeReviewStage with the FORCE_FINISH reason", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133509, + "status": "passed", + "title": "runs changeReviewStage with the FORCE_FINISH reason" + }, + { + "ancestorTitles": [ + "ReviewService", + "getStageHistory (review)" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewService getStageHistory (review) delegates to the stageHistory repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133511, + "status": "passed", + "title": "delegates to the stageHistory repository" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 12, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133631, + "loadTestEnvironmentEnd": 1779141132784, + "loadTestEnvironmentStart": 1779141132535, + "runtime": 846, + "setupAfterEnvEnd": 1779141133136, + "setupAfterEnvStart": 1779141132961, + "setupFilesEnd": 1779141132785, + "setupFilesStart": 1779141132785, + "slow": false, + "start": 1779141132785 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/controllers/reporting.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportingController", + "search" + ], + "duration": 8, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController search forwards query/actor and maps results", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133618, + "status": "passed", + "title": "forwards query/actor and maps results" + }, + { + "ancestorTitles": [ + "ReportingController", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController getById forwards id/actor to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133627, + "status": "passed", + "title": "forwards id/actor to the service" + }, + { + "ancestorTitles": [ + "ReportingController", + "getByReviewId" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController getByReviewId forwards review id/actor to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133627, + "status": "passed", + "title": "forwards review id/actor to the service" + }, + { + "ancestorTitles": [ + "ReportingController", + "getTextAnswers" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController getTextAnswers maps the text answers from the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133628, + "status": "passed", + "title": "maps the text answers from the service" + }, + { + "ancestorTitles": [ + "ReportingController", + "listReportAnalytics" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController listReportAnalytics maps every analytics row to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133629, + "status": "passed", + "title": "maps every analytics row to a response" + }, + { + "ancestorTitles": [ + "ReportingController", + "getReportAnalyticsById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController getReportAnalyticsById forwards id and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133629, + "status": "passed", + "title": "forwards id and maps the response" + }, + { + "ancestorTitles": [ + "ReportingController", + "listReportInsights" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController listReportInsights maps every insight to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133629, + "status": "passed", + "title": "maps every insight to a response" + }, + { + "ancestorTitles": [ + "ReportingController", + "getReportInsightById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController getReportInsightById forwards id and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133630, + "status": "passed", + "title": "forwards id and maps the response" + }, + { + "ancestorTitles": [ + "ReportingController", + "listReportComments" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController listReportComments maps every comment to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133630, + "status": "passed", + "title": "maps every comment to a response" + }, + { + "ancestorTitles": [ + "ReportingController", + "getReportCommentById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController getReportCommentById forwards id and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133630, + "status": "passed", + "title": "forwards id and maps the response" + }, + { + "ancestorTitles": [ + "ReportingController", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController create builds a domain comment from the dto and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141133630, + "status": "passed", + "title": "builds a domain comment from the dto and forwards to the service" + }, + { + "ancestorTitles": [ + "ReportingController", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportingController create defaults sentiment to null when omitted from the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133631, + "status": "passed", + "title": "defaults sentiment to null when omitted from the dto" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 19, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133669, + "loadTestEnvironmentEnd": 1779141132760, + "loadTestEnvironmentStart": 1779141132542, + "runtime": 909, + "setupAfterEnvEnd": 1779141133128, + "setupAfterEnvStart": 1779141132959, + "setupFilesEnd": 1779141132760, + "setupFilesStart": 1779141132760, + "slow": false, + "start": 1779141132760 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/controllers/reviews.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewController", + "create" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController create returns the http response for the created review", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133636, + "status": "passed", + "title": "returns the http response for the created review" + }, + { + "ancestorTitles": [ + "ReviewController", + "search" + ], + "duration": 8, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController search passes actor to the service and maps results", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133647, + "status": "passed", + "title": "passes actor to the service and maps results" + }, + { + "ancestorTitles": [ + "ReviewController", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController getById parses the id and forwards the actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133655, + "status": "passed", + "title": "parses the id and forwards the actor" + }, + { + "ancestorTitles": [ + "ReviewController", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController update passes actor and parsed id to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133656, + "status": "passed", + "title": "passes actor and parsed id to the service" + }, + { + "ancestorTitles": [ + "ReviewController", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController delete parses the id and calls the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133656, + "status": "passed", + "title": "parses the id and calls the service" + }, + { + "ancestorTitles": [ + "ReviewController", + "attachQuestion" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController attachQuestion forwards the dto with the parsed review id", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133656, + "status": "passed", + "title": "forwards the dto with the parsed review id" + }, + { + "ancestorTitles": [ + "ReviewController", + "listQuestions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController listQuestions forwards the request to listQuestionRelations and maps results", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133657, + "status": "passed", + "title": "forwards the request to listQuestionRelations and maps results" + }, + { + "ancestorTitles": [ + "ReviewController", + "detachQuestion" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController detachQuestion parses both path params", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133658, + "status": "passed", + "title": "parses both path params" + }, + { + "ancestorTitles": [ + "ReviewController", + "addAnswer" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController addAnswer forwards a numerical answer payload", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133658, + "status": "passed", + "title": "forwards a numerical answer payload" + }, + { + "ancestorTitles": [ + "ReviewController", + "listAnswers" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController listAnswers passes the respondentCategory query through", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133660, + "status": "passed", + "title": "passes the respondentCategory query through" + }, + { + "ancestorTitles": [ + "ReviewController", + "countAnswers" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController countAnswers returns the count from the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133660, + "status": "passed", + "title": "returns the count from the service" + }, + { + "ancestorTitles": [ + "ReviewController", + "addRespondent" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController addRespondent builds the payload from path id and body", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133661, + "status": "passed", + "title": "builds the payload from path id and body" + }, + { + "ancestorTitles": [ + "ReviewController", + "listRespondents" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController listRespondents forwards the call with actor and query", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133661, + "status": "passed", + "title": "forwards the call with actor and query" + }, + { + "ancestorTitles": [ + "ReviewController", + "updateRespondent" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController updateRespondent parses the relation id and forwards the dto and actor", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133664, + "status": "passed", + "title": "parses the relation id and forwards the dto and actor" + }, + { + "ancestorTitles": [ + "ReviewController", + "deleteRespondent" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController deleteRespondent parses the id and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133666, + "status": "passed", + "title": "parses the id and forwards the call" + }, + { + "ancestorTitles": [ + "ReviewController", + "addReviewer" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController addReviewer builds the payload from the path id and body", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133666, + "status": "passed", + "title": "builds the payload from the path id and body" + }, + { + "ancestorTitles": [ + "ReviewController", + "listReviewers" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController listReviewers forwards the call and maps results", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133668, + "status": "passed", + "title": "forwards the call and maps results" + }, + { + "ancestorTitles": [ + "ReviewController", + "deleteReviewer" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController deleteReviewer parses the id and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133668, + "status": "passed", + "title": "parses the id and forwards the call" + }, + { + "ancestorTitles": [ + "ReviewController", + "forceFinish" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewController forceFinish forwards the actor info and returns the refreshed review", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133668, + "status": "passed", + "title": "forwards the actor info and returns the refreshed review" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133785, + "loadTestEnvironmentEnd": 1779141133409, + "loadTestEnvironmentStart": 1779141133407, + "runtime": 376, + "setupAfterEnvEnd": 1779141133473, + "setupAfterEnvStart": 1779141133443, + "setupFilesEnd": 1779141133409, + "setupFilesStart": 1779141133409, + "slow": false, + "start": 1779141133409 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/presentation/controllers/identity-users.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "IdentityUsersController", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUsersController create forwards the dto to the service and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133779, + "status": "passed", + "title": "forwards the dto to the service and maps the response" + }, + { + "ancestorTitles": [ + "IdentityUsersController", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUsersController search maps every user to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133780, + "status": "passed", + "title": "maps every user to a response" + }, + { + "ancestorTitles": [ + "IdentityUsersController", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUsersController getById parses the id and requests roles inclusion", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133781, + "status": "passed", + "title": "parses the id and requests roles inclusion" + }, + { + "ancestorTitles": [ + "IdentityUsersController", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUsersController update parses the id and forwards the patch", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133781, + "status": "passed", + "title": "parses the id and forwards the patch" + }, + { + "ancestorTitles": [ + "IdentityUsersController", + "replaceRoles" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUsersController replaceRoles parses the id and forwards the roles list", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133781, + "status": "passed", + "title": "parses the id and forwards the roles list" + }, + { + "ancestorTitles": [ + "IdentityUsersController", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUsersController delete parses the id and calls service.delete", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133782, + "status": "passed", + "title": "parses the id and calls service.delete" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133820, + "loadTestEnvironmentEnd": 1779141133389, + "loadTestEnvironmentStart": 1779141133385, + "runtime": 430, + "setupAfterEnvEnd": 1779141133475, + "setupAfterEnvStart": 1779141133421, + "setupFilesEnd": 1779141133390, + "setupFilesStart": 1779141133390, + "slow": false, + "start": 1779141133390 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/presentation/controllers/positions.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionsController", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController create forwards the dto to the service and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133814, + "status": "passed", + "title": "forwards the dto to the service and maps the response" + }, + { + "ancestorTitles": [ + "PositionsController", + "search" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController search maps every position to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133816, + "status": "passed", + "title": "maps every position to a response" + }, + { + "ancestorTitles": [ + "PositionsController", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController getById parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133816, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "PositionsController", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController update parses the id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133817, + "status": "passed", + "title": "parses the id and forwards the dto" + }, + { + "ancestorTitles": [ + "PositionsController", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController delete parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133817, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "PositionsController", + "linkSubordinate" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController linkSubordinate links and returns the subordinate position", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133818, + "status": "passed", + "title": "links and returns the subordinate position" + }, + { + "ancestorTitles": [ + "PositionsController", + "unlinkSubordinate" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController unlinkSubordinate parses both ids and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133818, + "status": "passed", + "title": "parses both ids and forwards the call" + }, + { + "ancestorTitles": [ + "PositionsController", + "listSubordinates" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController listSubordinates maps every subordinate to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133818, + "status": "passed", + "title": "maps every subordinate to a response" + }, + { + "ancestorTitles": [ + "PositionsController", + "listSuperiors" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController listSuperiors maps every superior to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133819, + "status": "passed", + "title": "maps every superior to a response" + }, + { + "ancestorTitles": [ + "PositionsController", + "listCompetences" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionsController listCompetences forwards the call and returns the list", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133819, + "status": "passed", + "title": "forwards the call and returns the list" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133826, + "loadTestEnvironmentEnd": 1779141133478, + "loadTestEnvironmentStart": 1779141133475, + "runtime": 348, + "setupAfterEnvEnd": 1779141133558, + "setupAfterEnvStart": 1779141133513, + "setupFilesEnd": 1779141133478, + "setupFilesStart": 1779141133478, + "slow": false, + "start": 1779141133478 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/controllers/cluster-scores.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoresController", + "upsert" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoresController upsert forwards the dto and returns a mapped response", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133825, + "status": "passed", + "title": "forwards the dto and returns a mapped response" + }, + { + "ancestorTitles": [ + "ClusterScoresController", + "list" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoresController list maps each score to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133825, + "status": "passed", + "title": "maps each score to a response" + }, + { + "ancestorTitles": [ + "ClusterScoresController", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoresController getById returns the with-relations response", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133826, + "status": "passed", + "title": "returns the with-relations response" + }, + { + "ancestorTitles": [ + "ClusterScoresController", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoresController delete parses the id and calls remove", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133826, + "status": "passed", + "title": "parses the id and calls remove" + }, + { + "ancestorTitles": [ + "ClusterScoresController", + "getByCycleId" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoresController getByCycleId maps every aggregate to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133826, + "status": "passed", + "title": "maps every aggregate to a response" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 7, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133858, + "loadTestEnvironmentEnd": 1779141133426, + "loadTestEnvironmentStart": 1779141133422, + "runtime": 431, + "setupAfterEnvEnd": 1779141133511, + "setupAfterEnvStart": 1779141133464, + "setupFilesEnd": 1779141133427, + "setupFilesStart": 1779141133427, + "slow": false, + "start": 1779141133427 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/controllers/strategic-reporting.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportingController", + "search" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingController search forwards query/actor and maps results", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133850, + "status": "passed", + "title": "forwards query/actor and maps results" + }, + { + "ancestorTitles": [ + "StrategicReportingController", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingController getById forwards id/actor to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133853, + "status": "passed", + "title": "forwards id/actor to the service" + }, + { + "ancestorTitles": [ + "StrategicReportingController", + "getByCycleId" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingController getByCycleId forwards cycle id/actor to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133854, + "status": "passed", + "title": "forwards cycle id/actor to the service" + }, + { + "ancestorTitles": [ + "StrategicReportingController", + "listStrategicReportAnalytics" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingController listStrategicReportAnalytics maps every analytics row to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133855, + "status": "passed", + "title": "maps every analytics row to a response" + }, + { + "ancestorTitles": [ + "StrategicReportingController", + "getStrategicReportAnalyticsById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingController getStrategicReportAnalyticsById forwards id and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133856, + "status": "passed", + "title": "forwards id and maps the response" + }, + { + "ancestorTitles": [ + "StrategicReportingController", + "listStrategicReportInsights" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingController listStrategicReportInsights maps every insight to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133856, + "status": "passed", + "title": "maps every insight to a response" + }, + { + "ancestorTitles": [ + "StrategicReportingController", + "getStrategicReportInsightById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportingController getStrategicReportInsightById forwards id and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133857, + "status": "passed", + "title": "forwards id and maps the response" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 11, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133871, + "loadTestEnvironmentEnd": 1779141133499, + "loadTestEnvironmentStart": 1779141133496, + "runtime": 372, + "setupAfterEnvEnd": 1779141133564, + "setupAfterEnvStart": 1779141133522, + "setupFilesEnd": 1779141133499, + "setupFilesStart": 1779141133499, + "slow": false, + "start": 1779141133499 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/presentation/controllers/question-templates.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionTemplatesController", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController create forwards the dto and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133867, + "status": "passed", + "title": "forwards the dto and maps the response" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "search" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController search maps every template to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133867, + "status": "passed", + "title": "maps every template to a response" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController getById parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133867, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController update parses the id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133868, + "status": "passed", + "title": "parses the id and forwards the dto" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController delete parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133868, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "attachPosition" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController attachPosition parses the path id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133869, + "status": "passed", + "title": "parses the path id and forwards the dto" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "detachPosition" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController detachPosition parses both ids and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133869, + "status": "passed", + "title": "parses both ids and forwards the call" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "listPositions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController listPositions forwards the call and returns the result", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133870, + "status": "passed", + "title": "forwards the call and returns the result" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "attachCompetence" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController attachCompetence parses the path id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133870, + "status": "passed", + "title": "parses the path id and forwards the dto" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "detachCompetence" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController detachCompetence parses both ids and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133870, + "status": "passed", + "title": "parses both ids and forwards the call" + }, + { + "ancestorTitles": [ + "QuestionTemplatesController", + "listCompetences" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplatesController listCompetences forwards the call and returns the result", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133871, + "status": "passed", + "title": "forwards the call and returns the result" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133892, + "loadTestEnvironmentEnd": 1779141133412, + "loadTestEnvironmentStart": 1779141133410, + "runtime": 478, + "setupAfterEnvEnd": 1779141133490, + "setupAfterEnvStart": 1779141133440, + "setupFilesEnd": 1779141133414, + "setupFilesStart": 1779141133414, + "slow": false, + "start": 1779141133414 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/controllers/cluster-score-analytics.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreAnalyticsController", + "upsert" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsController upsert forwards every dto field and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133885, + "status": "passed", + "title": "forwards every dto field and maps the response" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsController", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsController search maps each analytics row to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133888, + "status": "passed", + "title": "maps each analytics row to a response" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsController", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsController getById parses the id and calls the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133889, + "status": "passed", + "title": "parses the id and calls the service" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsController", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsController update parses the id and forwards the patch", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133889, + "status": "passed", + "title": "parses the id and forwards the patch" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsController", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsController delete parses the id and calls the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133891, + "status": "passed", + "title": "parses the id and calls the service" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsController", + "getByCycleId" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsController getByCycleId maps each analytics row to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133891, + "status": "passed", + "title": "maps each analytics row to a response" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 11, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133927, + "loadTestEnvironmentEnd": 1779141133643, + "loadTestEnvironmentStart": 1779141133637, + "runtime": 284, + "setupAfterEnvEnd": 1779141133721, + "setupAfterEnvStart": 1779141133677, + "setupFilesEnd": 1779141133643, + "setupFilesStart": 1779141133643, + "slow": false, + "start": 1779141133643 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/presentation/controllers/competences.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetencesController", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController create forwards the dto and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133920, + "status": "passed", + "title": "forwards the dto and maps the response" + }, + { + "ancestorTitles": [ + "CompetencesController", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController search maps every competence to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133921, + "status": "passed", + "title": "maps every competence to a response" + }, + { + "ancestorTitles": [ + "CompetencesController", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController getById parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133922, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "CompetencesController", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController update parses the id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133923, + "status": "passed", + "title": "parses the id and forwards the dto" + }, + { + "ancestorTitles": [ + "CompetencesController", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController delete parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133924, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "CompetencesController", + "attachPosition" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController attachPosition parses the path id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133924, + "status": "passed", + "title": "parses the path id and forwards the dto" + }, + { + "ancestorTitles": [ + "CompetencesController", + "detachPosition" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController detachPosition parses both ids and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133924, + "status": "passed", + "title": "parses both ids and forwards the call" + }, + { + "ancestorTitles": [ + "CompetencesController", + "listPositions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController listPositions forwards the call and returns the result", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133924, + "status": "passed", + "title": "forwards the call and returns the result" + }, + { + "ancestorTitles": [ + "CompetencesController", + "attachQuestionTemplate" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController attachQuestionTemplate parses the path id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133925, + "status": "passed", + "title": "parses the path id and forwards the dto" + }, + { + "ancestorTitles": [ + "CompetencesController", + "detachQuestionTemplate" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController detachQuestionTemplate parses both ids and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133925, + "status": "passed", + "title": "parses both ids and forwards the call" + }, + { + "ancestorTitles": [ + "CompetencesController", + "listQuestionTemplates" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetencesController listQuestionTemplates forwards the call and returns the result", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133926, + "status": "passed", + "title": "forwards the call and returns the result" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133927, + "loadTestEnvironmentEnd": 1779141133534, + "loadTestEnvironmentStart": 1779141133529, + "runtime": 393, + "setupAfterEnvEnd": 1779141133609, + "setupAfterEnvStart": 1779141133567, + "setupFilesEnd": 1779141133534, + "setupFilesStart": 1779141133534, + "slow": false, + "start": 1779141133534 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/controllers/questions.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionsController", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionsController create maps the result to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133923, + "status": "passed", + "title": "maps the result to a response" + }, + { + "ancestorTitles": [ + "QuestionsController", + "search" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionsController search maps every question to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133924, + "status": "passed", + "title": "maps every question to a response" + }, + { + "ancestorTitles": [ + "QuestionsController", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionsController delete parses the id and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133926, + "status": "passed", + "title": "parses the id and forwards the call" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 8, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141133951, + "loadTestEnvironmentEnd": 1779141133692, + "loadTestEnvironmentStart": 1779141133687, + "runtime": 259, + "setupAfterEnvEnd": 1779141133799, + "setupAfterEnvStart": 1779141133731, + "setupFilesEnd": 1779141133692, + "setupFilesStart": 1779141133692, + "slow": false, + "start": 1779141133692 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/presentation/controllers/teams.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamsController", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamsController create forwards the dto and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133945, + "status": "passed", + "title": "forwards the dto and maps the response" + }, + { + "ancestorTitles": [ + "TeamsController", + "search" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamsController search maps every team to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133947, + "status": "passed", + "title": "maps every team to a response" + }, + { + "ancestorTitles": [ + "TeamsController", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamsController getById parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133947, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "TeamsController", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamsController update parses the id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133947, + "status": "passed", + "title": "parses the id and forwards the dto" + }, + { + "ancestorTitles": [ + "TeamsController", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamsController delete parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133948, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "TeamsController", + "addMember" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamsController addMember parses the id, forwards the dto with withUser, and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141133948, + "status": "passed", + "title": "parses the id, forwards the dto with withUser, and maps the response" + }, + { + "ancestorTitles": [ + "TeamsController", + "listMembers" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamsController listMembers maps every membership to a response and requests user data", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133949, + "status": "passed", + "title": "maps every membership to a response and requests user data" + }, + { + "ancestorTitles": [ + "TeamsController", + "removeMember" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamsController removeMember parses both ids and forwards the call", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141133951, + "status": "passed", + "title": "parses both ids and forwards the call" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134006, + "loadTestEnvironmentEnd": 1779141133866, + "loadTestEnvironmentStart": 1779141133863, + "runtime": 140, + "setupAfterEnvEnd": 1779141133955, + "setupAfterEnvStart": 1779141133893, + "setupFilesEnd": 1779141133866, + "setupFilesStart": 1779141133866, + "slow": false, + "start": 1779141133866 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/presentation/mappers/user.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "UserHttpMapper", + "toResponse" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 14, + "retryReasons": [], + "startAt": 1779141134003, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "UserHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserHttpMapper toResponse returns null for missing optional fields and [] for roles", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134005, + "status": "passed", + "title": "returns null for missing optional fields and [] for roles" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134006, + "loadTestEnvironmentEnd": 1779141133794, + "loadTestEnvironmentStart": 1779141133789, + "runtime": 212, + "setupAfterEnvEnd": 1779141133868, + "setupAfterEnvStart": 1779141133817, + "setupFilesEnd": 1779141133794, + "setupFilesStart": 1779141133794, + "slow": false, + "start": 1779141133794 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/controllers/cycles.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CyclesController", + "create" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CyclesController create returns the response shape produced by the http mapper", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141133999, + "status": "passed", + "title": "returns the response shape produced by the http mapper" + }, + { + "ancestorTitles": [ + "CyclesController", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CyclesController search maps every cycle to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134001, + "status": "passed", + "title": "maps every cycle to a response" + }, + { + "ancestorTitles": [ + "CyclesController", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CyclesController getById parses the id as a number and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134002, + "status": "passed", + "title": "parses the id as a number and forwards to the service" + }, + { + "ancestorTitles": [ + "CyclesController", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CyclesController update parses the id and forwards the patch", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134003, + "status": "passed", + "title": "parses the id and forwards the patch" + }, + { + "ancestorTitles": [ + "CyclesController", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CyclesController delete parses the id and calls service.delete", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134003, + "status": "passed", + "title": "parses the id and calls service.delete" + }, + { + "ancestorTitles": [ + "CyclesController", + "forceFinish" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CyclesController forceFinish passes actor id and full name to the service and returns the refreshed cycle", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134004, + "status": "passed", + "title": "passes actor id and full name to the service and returns the refreshed cycle" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134011, + "loadTestEnvironmentEnd": 1779141133841, + "loadTestEnvironmentStart": 1779141133836, + "runtime": 170, + "setupAfterEnvEnd": 1779141133920, + "setupAfterEnvStart": 1779141133863, + "setupFilesEnd": 1779141133841, + "setupFilesStart": 1779141133841, + "slow": false, + "start": 1779141133841 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/presentation/controllers/clusters.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClustersController", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClustersController create forwards the dto and maps the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134007, + "status": "passed", + "title": "forwards the dto and maps the response" + }, + { + "ancestorTitles": [ + "ClustersController", + "search" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClustersController search maps every cluster to a response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134008, + "status": "passed", + "title": "maps every cluster to a response" + }, + { + "ancestorTitles": [ + "ClustersController", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClustersController getById parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134008, + "status": "passed", + "title": "parses the id and forwards to the service" + }, + { + "ancestorTitles": [ + "ClustersController", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClustersController update parses the id and forwards the dto", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134008, + "status": "passed", + "title": "parses the id and forwards the dto" + }, + { + "ancestorTitles": [ + "ClustersController", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClustersController delete parses the id and forwards to the service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134009, + "status": "passed", + "title": "parses the id and forwards to the service" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134016, + "loadTestEnvironmentEnd": 1779141133902, + "loadTestEnvironmentStart": 1779141133899, + "runtime": 114, + "setupAfterEnvEnd": 1779141133975, + "setupAfterEnvStart": 1779141133945, + "setupFilesEnd": 1779141133902, + "setupFilesStart": 1779141133902, + "slow": false, + "start": 1779141133902 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/services/report-analytics.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportAnalyticsService", + "getByReportId" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService getByReportId returns analytics after the access check passes", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134007, + "status": "passed", + "title": "returns analytics after the access check passes" + }, + { + "ancestorTitles": [ + "ReportAnalyticsService", + "getByReportId" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService getByReportId throws NotFoundException when repository returns null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134012, + "status": "passed", + "title": "throws NotFoundException when repository returns null" + }, + { + "ancestorTitles": [ + "ReportAnalyticsService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService getById throws NotFoundException when the analytics row is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134013, + "status": "passed", + "title": "throws NotFoundException when the analytics row is missing" + }, + { + "ancestorTitles": [ + "ReportAnalyticsService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsService getById re-checks access via the parent report", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134014, + "status": "passed", + "title": "re-checks access via the parent report" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134017, + "loadTestEnvironmentEnd": 1779141133828, + "loadTestEnvironmentStart": 1779141133826, + "runtime": 189, + "setupAfterEnvEnd": 1779141133940, + "setupAfterEnvStart": 1779141133859, + "setupFilesEnd": 1779141133828, + "setupFilesStart": 1779141133828, + "slow": false, + "start": 1779141133828 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/listeners/review-stage.listener.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageListener (reporting)" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (reporting) ignores transitions that are not FINISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134000, + "status": "passed", + "title": "ignores transitions that are not FINISHED" + }, + { + "ancestorTitles": [ + "ReviewStageListener (reporting)" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (reporting) runs the report pipeline when FINISHED: PREPARING_REPORT → generate → PROCESSING_BY_HR", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134003, + "status": "passed", + "title": "runs the report pipeline when FINISHED: PREPARING_REPORT → generate → PROCESSING_BY_HR" + }, + { + "ancestorTitles": [ + "ReviewStageListener (reporting)" + ], + "duration": 9, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener (reporting) swallows errors from the report generation pipeline", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134007, + "status": "passed", + "title": "swallows errors from the report generation pipeline" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134028, + "loadTestEnvironmentEnd": 1779141133888, + "loadTestEnvironmentStart": 1779141133883, + "runtime": 140, + "setupAfterEnvEnd": 1779141133977, + "setupAfterEnvStart": 1779141133927, + "setupFilesEnd": 1779141133888, + "setupFilesStart": 1779141133888, + "slow": false, + "start": 1779141133888 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/mappers/report-insight.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportInsightHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightHttpMapper toResponse maps every domain field, rounding decimals to 4dp", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134026, + "status": "passed", + "title": "maps every domain field, rounding decimals to 4dp" + }, + { + "ancestorTitles": [ + "ReportInsightHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightHttpMapper toResponse returns null for missing decimal fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134027, + "status": "passed", + "title": "returns null for missing decimal fields" + }, + { + "ancestorTitles": [ + "ReportInsightHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightHttpMapper roundScore rounds a Decimal value to 4 decimal places", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134027, + "status": "passed", + "title": "rounds a Decimal value to 4 decimal places" + }, + { + "ancestorTitles": [ + "ReportInsightHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightHttpMapper roundScore returns null for null and undefined", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134027, + "status": "passed", + "title": "returns null for null and undefined" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134047, + "loadTestEnvironmentEnd": 1779141133936, + "loadTestEnvironmentStart": 1779141133931, + "runtime": 110, + "setupAfterEnvEnd": 1779141133976, + "setupAfterEnvStart": 1779141133949, + "setupFilesEnd": 1779141133937, + "setupFilesStart": 1779141133937, + "slow": false, + "start": 1779141133937 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/listeners/cycle-stage.listener.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageListener (reporting)" + ], + "duration": 6, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener (reporting) ignores transitions that are not FINISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134029, + "status": "passed", + "title": "ignores transitions that are not FINISHED" + }, + { + "ancestorTitles": [ + "CycleStageListener (reporting)" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener (reporting) runs the report pipeline when FINISHED: PREPARING_REPORT → generate → PUBLISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134035, + "status": "passed", + "title": "runs the report pipeline when FINISHED: PREPARING_REPORT → generate → PUBLISHED" + }, + { + "ancestorTitles": [ + "CycleStageListener (reporting)" + ], + "duration": 8, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener (reporting) swallows errors from the report generation pipeline", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134038, + "status": "passed", + "title": "swallows errors from the report generation pipeline" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134082, + "loadTestEnvironmentEnd": 1779141133958, + "loadTestEnvironmentStart": 1779141133955, + "runtime": 124, + "setupAfterEnvEnd": 1779141134017, + "setupAfterEnvStart": 1779141133982, + "setupFilesEnd": 1779141133958, + "setupFilesStart": 1779141133958, + "slow": false, + "start": 1779141133958 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/presentation/mappers/question-template.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionTemplateHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134081, + "status": "passed", + "title": "maps every domain field onto the response" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134113, + "loadTestEnvironmentEnd": 1779141134017, + "loadTestEnvironmentStart": 1779141134015, + "runtime": 96, + "setupAfterEnvEnd": 1779141134060, + "setupAfterEnvStart": 1779141134037, + "setupFilesEnd": 1779141134017, + "setupFilesStart": 1779141134017, + "slow": false, + "start": 1779141134017 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report-analytics.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportAnalyticsHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsHttpMapper toResponse maps every domain field, rounding decimals to 4dp", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134112, + "status": "passed", + "title": "maps every domain field, rounding decimals to 4dp" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsHttpMapper toResponse returns null for missing decimal fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134113, + "status": "passed", + "title": "returns null for missing decimal fields" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsHttpMapper roundScore rounds a Decimal value to 4 decimal places", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134113, + "status": "passed", + "title": "rounds a Decimal value to 4 decimal places" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsHttpMapper roundScore returns null for null and undefined", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134113, + "status": "passed", + "title": "returns null for null and undefined" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134128, + "loadTestEnvironmentEnd": 1779141133934, + "loadTestEnvironmentStart": 1779141133930, + "runtime": 194, + "setupAfterEnvEnd": 1779141133977, + "setupAfterEnvStart": 1779141133952, + "setupFilesEnd": 1779141133934, + "setupFilesStart": 1779141133934, + "slow": false, + "start": 1779141133934 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/services/review-scheduler.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkReviewDeadlines" + ], + "duration": 23, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkReviewDeadlines transitions IN_PROGRESS reviews to FINISHED when the cycle deadline has passed", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134076, + "status": "passed", + "title": "transitions IN_PROGRESS reviews to FINISHED when the cycle deadline has passed" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkReviewDeadlines" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkReviewDeadlines skips cycles without a responseDeadline", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134099, + "status": "passed", + "title": "skips cycles without a responseDeadline" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkReviewDeadlines" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkReviewDeadlines does nothing when the deadline is in the future", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134102, + "status": "passed", + "title": "does nothing when the deadline is in the future" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkReviewDeadlines" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkReviewDeadlines continues processing remaining reviews when one transition fails", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134104, + "status": "passed", + "title": "continues processing remaining reviews when one transition fails" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkReviewDeadlines" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkReviewDeadlines swallows errors from the cycle search and does not throw", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134104, + "status": "passed", + "title": "swallows errors from the cycle search and does not throw" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkCycleDeadlines" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkCycleDeadlines force-finishes cycles whose endDate has passed", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134107, + "status": "passed", + "title": "force-finishes cycles whose endDate has passed" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkCycleDeadlines" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkCycleDeadlines force-finishes cycles whose responseDeadline has passed even when endDate is in the future", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134109, + "status": "passed", + "title": "force-finishes cycles whose responseDeadline has passed even when endDate is in the future" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkCycleDeadlines" + ], + "duration": 8, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkCycleDeadlines skips cycles where no deadline has passed yet", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134114, + "status": "passed", + "title": "skips cycles where no deadline has passed yet" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkCycleDeadlines" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkCycleDeadlines continues processing remaining cycles when one fails to finish", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134122, + "status": "passed", + "title": "continues processing remaining cycles when one fails to finish" + }, + { + "ancestorTitles": [ + "ReviewSchedulerService", + "checkCycleDeadlines" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewSchedulerService checkCycleDeadlines swallows errors from the cycle search and does not throw", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134126, + "status": "passed", + "title": "swallows errors from the cycle search and does not throw" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134129, + "loadTestEnvironmentEnd": 1779141134032, + "loadTestEnvironmentStart": 1779141134031, + "runtime": 96, + "setupAfterEnvEnd": 1779141134081, + "setupAfterEnvStart": 1779141134050, + "setupFilesEnd": 1779141134033, + "setupFilesStart": 1779141134033, + "slow": false, + "start": 1779141134033 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/reviewer.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewerHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134128, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "ReviewerHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerHttpMapper toResponse returns null for missing team fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134129, + "status": "passed", + "title": "returns null for missing team fields" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134132, + "loadTestEnvironmentEnd": 1779141134022, + "loadTestEnvironmentStart": 1779141134018, + "runtime": 110, + "setupAfterEnvEnd": 1779141134074, + "setupAfterEnvStart": 1779141134047, + "setupFilesEnd": 1779141134022, + "setupFilesStart": 1779141134022, + "slow": false, + "start": 1779141134022 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/cluster-score.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreHttpMapper toResponse maps the domain fields and rounds score to 4dp", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134130, + "status": "passed", + "title": "maps the domain fields and rounds score to 4dp" + }, + { + "ancestorTitles": [ + "ClusterScoreHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreHttpMapper toResponse emits null cycleId when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134131, + "status": "passed", + "title": "emits null cycleId when missing" + }, + { + "ancestorTitles": [ + "ClusterScoreHttpMapper", + "toResponseWithRelations" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreHttpMapper toResponseWithRelations includes the cluster and ratee in the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134132, + "status": "passed", + "title": "includes the cluster and ratee in the response" + }, + { + "ancestorTitles": [ + "ClusterScoreHttpMapper", + "toScoreRoundedNumber" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreHttpMapper toScoreRoundedNumber rounds a Decimal to a 4-decimal-place number", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134132, + "status": "passed", + "title": "rounds a Decimal to a 4-decimal-place number" + }, + { + "ancestorTitles": [ + "ClusterScoreHttpMapper", + "toScoreRoundedNumber" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreHttpMapper toScoreRoundedNumber rounds a plain number to a 4-decimal-place number", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134132, + "status": "passed", + "title": "rounds a plain number to a 4-decimal-place number" + }, + { + "ancestorTitles": [ + "ClusterScoreHttpMapper", + "toScoreRoundedNumber" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreHttpMapper toScoreRoundedNumber returns null when the input is null or undefined", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134132, + "status": "passed", + "title": "returns null when the input is null or undefined" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 19, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134137, + "loadTestEnvironmentEnd": 1779141134023, + "loadTestEnvironmentStart": 1779141134020, + "runtime": 113, + "setupAfterEnvEnd": 1779141134082, + "setupAfterEnvStart": 1779141134044, + "setupFilesEnd": 1779141134024, + "setupFilesStart": 1779141134024, + "slow": false, + "start": 1779141134024 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/application/services/identity-user.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "IdentityUserService", + "create" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService create throws BadRequestException when a user with the email already exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134114, + "status": "passed", + "title": "throws BadRequestException when a user with the email already exists" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService create builds a domain with default status, ids, and roles when not supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134120, + "status": "passed", + "title": "builds a domain with default status, ids, and roles when not supplied" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService create honours the supplied fullName when present", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134121, + "status": "passed", + "title": "honours the supplied fullName when present" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService create emits user.created with the persisted id and email", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134122, + "status": "passed", + "title": "emits user.created with the persisted id and email" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService create does not emit user.created when the persisted user has no id", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134123, + "status": "passed", + "title": "does not emit user.created when the persisted user has no id" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService search delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134123, + "status": "passed", + "title": "delegates to the repository" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService getById returns the user when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134124, + "status": "passed", + "title": "returns the user when found" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService getById forwards the withRoles option", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134125, + "status": "passed", + "title": "forwards the withRoles option" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134126, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "findByEmail" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService findByEmail delegates to the repository with the supplied opts", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134127, + "status": "passed", + "title": "delegates to the repository with the supplied opts" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService update throws NotFoundException when the user does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134129, + "status": "passed", + "title": "throws NotFoundException when the user does not exist" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService update rebuilds fullName when any name part is patched", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134130, + "status": "passed", + "title": "rebuilds fullName when any name part is patched" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService update does not include fullName when no name part is patched", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134130, + "status": "passed", + "title": "does not include fullName when no name part is patched" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "delete" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService delete throws NotFoundException when the user is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134131, + "status": "passed", + "title": "throws NotFoundException when the user is missing" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService delete deletes when the user exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134133, + "status": "passed", + "title": "deletes when the user exists" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "replaceRoles" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService replaceRoles throws BadRequestException when an unknown role is requested", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134134, + "status": "passed", + "title": "throws BadRequestException when an unknown role is requested" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "replaceRoles" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService replaceRoles deduplicates roles and replaces them via the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134135, + "status": "passed", + "title": "deduplicates roles and replaces them via the repository" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "upsertExternalUser" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService upsertExternalUser returns the existing user when one with the email already exists", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134135, + "status": "passed", + "title": "returns the existing user when one with the email already exists" + }, + { + "ancestorTitles": [ + "IdentityUserService", + "upsertExternalUser" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityUserService upsertExternalUser creates a new EMPLOYEE-role user and emits user.created when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134136, + "status": "passed", + "title": "creates a new EMPLOYEE-role user and emits user.created when missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134141, + "loadTestEnvironmentEnd": 1779141134013, + "loadTestEnvironmentStart": 1779141134011, + "runtime": 128, + "setupAfterEnvEnd": 1779141134064, + "setupAfterEnvStart": 1779141134038, + "setupFilesEnd": 1779141134013, + "setupFilesStart": 1779141134013, + "slow": false, + "start": 1779141134013 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/presentation/controllers/identity-roles.controller.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "IdentityRolesController", + "list" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityRolesController list maps every role onto a RoleResponse", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134140, + "status": "passed", + "title": "maps every role onto a RoleResponse" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134152, + "loadTestEnvironmentEnd": 1779141134012, + "loadTestEnvironmentStart": 1779141134009, + "runtime": 140, + "setupAfterEnvEnd": 1779141134072, + "setupAfterEnvStart": 1779141134031, + "setupFilesEnd": 1779141134012, + "setupFilesStart": 1779141134012, + "slow": false, + "start": 1779141134012 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/services/report-comment.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportCommentService", + "getByReportId" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService getByReportId returns comments after asserting access via ReportingService", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134127, + "status": "passed", + "title": "returns comments after asserting access via ReportingService" + }, + { + "ancestorTitles": [ + "ReportCommentService", + "getByReportId" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService getByReportId throws NotFoundException when no comments are found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134132, + "status": "passed", + "title": "throws NotFoundException when no comments are found" + }, + { + "ancestorTitles": [ + "ReportCommentService", + "getByReportId" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService getByReportId propagates errors from the reporting access check", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134136, + "status": "passed", + "title": "propagates errors from the reporting access check" + }, + { + "ancestorTitles": [ + "ReportCommentService", + "getById" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService getById returns the comment when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134136, + "status": "passed", + "title": "returns the comment when found" + }, + { + "ancestorTitles": [ + "ReportCommentService", + "getById" + ], + "duration": 6, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134139, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "ReportCommentService", + "create" + ], + "duration": 6, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentService create delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134145, + "status": "passed", + "title": "delegates to the repository" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134207, + "loadTestEnvironmentEnd": 1779141134051, + "loadTestEnvironmentStart": 1779141134049, + "runtime": 156, + "setupAfterEnvEnd": 1779141134132, + "setupAfterEnvStart": 1779141134076, + "setupFilesEnd": 1779141134051, + "setupFilesStart": 1779141134051, + "slow": false, + "start": 1779141134051 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/cycle.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 15, + "retryReasons": [], + "startAt": 1779141134205, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "CycleHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleHttpMapper toResponse emits explicit null values for missing optional fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134206, + "status": "passed", + "title": "emits explicit null values for missing optional fields" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134248, + "loadTestEnvironmentEnd": 1779141134124, + "loadTestEnvironmentStart": 1779141134121, + "runtime": 124, + "setupAfterEnvEnd": 1779141134172, + "setupAfterEnvStart": 1779141134141, + "setupFilesEnd": 1779141134124, + "setupFilesStart": 1779141134124, + "slow": false, + "start": 1779141134124 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/listeners/cycle-stage.listener.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageListener", + "handleCycleStageChanged" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener handleCycleStageChanged emits cycle.stage.processed without analytics when the new stage is not FINISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134240, + "status": "passed", + "title": "emits cycle.stage.processed without analytics when the new stage is not FINISHED" + }, + { + "ancestorTitles": [ + "CycleStageListener", + "handleCycleStageChanged" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener handleCycleStageChanged does not run analytics when the cycle has no reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134243, + "status": "passed", + "title": "does not run analytics when the cycle has no reviews" + }, + { + "ancestorTitles": [ + "CycleStageListener", + "handleCycleStageChanged" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener handleCycleStageChanged runs analytics generation and emits the processed event when FINISHED with reviews", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134245, + "status": "passed", + "title": "runs analytics generation and emits the processed event when FINISHED with reviews" + }, + { + "ancestorTitles": [ + "CycleStageListener", + "handleCycleStageChanged" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageListener handleCycleStageChanged still emits the processed event when analytics generation fails", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134246, + "status": "passed", + "title": "still emits the processed event when analytics generation fails" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134252, + "loadTestEnvironmentEnd": 1779141134131, + "loadTestEnvironmentStart": 1779141134129, + "runtime": 121, + "setupAfterEnvEnd": 1779141134195, + "setupAfterEnvStart": 1779141134155, + "setupFilesEnd": 1779141134131, + "setupFilesStart": 1779141134131, + "slow": false, + "start": 1779141134131 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/mappers/report-analytics.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportAnalyticsHttpMapper", + "toResponse" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsHttpMapper toResponse maps every domain field, rounding decimals to 4dp", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134250, + "status": "passed", + "title": "maps every domain field, rounding decimals to 4dp" + }, + { + "ancestorTitles": [ + "ReportAnalyticsHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsHttpMapper toResponse returns null for missing decimal fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134252, + "status": "passed", + "title": "returns null for missing decimal fields" + }, + { + "ancestorTitles": [ + "ReportAnalyticsHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsHttpMapper roundScore rounds a Decimal value to 4 decimal places", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134252, + "status": "passed", + "title": "rounds a Decimal value to 4 decimal places" + }, + { + "ancestorTitles": [ + "ReportAnalyticsHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsHttpMapper roundScore accepts a plain number", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134252, + "status": "passed", + "title": "accepts a plain number" + }, + { + "ancestorTitles": [ + "ReportAnalyticsHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsHttpMapper roundScore returns null for null or undefined", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134252, + "status": "passed", + "title": "returns null for null or undefined" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134254, + "loadTestEnvironmentEnd": 1779141134098, + "loadTestEnvironmentStart": 1779141134094, + "runtime": 156, + "setupAfterEnvEnd": 1779141134159, + "setupAfterEnvStart": 1779141134113, + "setupFilesEnd": 1779141134098, + "setupFilesStart": 1779141134098, + "slow": false, + "start": 1779141134098 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/application/listeners/cycle-stage-notification.listener.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageNotificationListener" + ], + "duration": 10, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageNotificationListener triggers the strategic-report email when the cycle reaches PUBLISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134236, + "status": "passed", + "title": "triggers the strategic-report email when the cycle reaches PUBLISHED" + }, + { + "ancestorTitles": [ + "CycleStageNotificationListener" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageNotificationListener ignores other cycle stage transitions", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134246, + "status": "passed", + "title": "ignores other cycle stage transitions" + }, + { + "ancestorTitles": [ + "CycleStageNotificationListener" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageNotificationListener swallows errors thrown by the notification service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134249, + "status": "passed", + "title": "swallows errors thrown by the notification service" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 11, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134266, + "loadTestEnvironmentEnd": 1779141134157, + "loadTestEnvironmentStart": 1779141134154, + "runtime": 109, + "setupAfterEnvEnd": 1779141134221, + "setupAfterEnvStart": 1779141134175, + "setupFilesEnd": 1779141134157, + "setupFilesStart": 1779141134157, + "slow": false, + "start": 1779141134157 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/application/services/position.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionService", + "create" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService create persists the position with null fallback for description", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134248, + "status": "passed", + "title": "persists the position with null fallback for description" + }, + { + "ancestorTitles": [ + "PositionService", + "create" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService create passes through optional description", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134253, + "status": "passed", + "title": "passes through optional description" + }, + { + "ancestorTitles": [ + "PositionService", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService search delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134255, + "status": "passed", + "title": "delegates to the repository" + }, + { + "ancestorTitles": [ + "PositionService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService getById returns the position when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134256, + "status": "passed", + "title": "returns the position when found" + }, + { + "ancestorTitles": [ + "PositionService", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134257, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "PositionService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService update throws NotFoundException when the position does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134257, + "status": "passed", + "title": "throws NotFoundException when the position does not exist" + }, + { + "ancestorTitles": [ + "PositionService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService update forwards the patch to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134258, + "status": "passed", + "title": "forwards the patch to the repository" + }, + { + "ancestorTitles": [ + "PositionService", + "delete" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService delete throws NotFoundException when the position does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134259, + "status": "passed", + "title": "throws NotFoundException when the position does not exist" + }, + { + "ancestorTitles": [ + "PositionService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService delete rejects when there are competences linked to the position", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134261, + "status": "passed", + "title": "rejects when there are competences linked to the position" + }, + { + "ancestorTitles": [ + "PositionService", + "delete" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService delete deletes the position when no competences are linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134262, + "status": "passed", + "title": "deletes the position when no competences are linked" + }, + { + "ancestorTitles": [ + "PositionService", + "listCompetences" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionService listCompetences maps relations to competence ids", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134265, + "status": "passed", + "title": "maps relations to competence ids" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 19, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134289, + "loadTestEnvironmentEnd": 1779141134141, + "loadTestEnvironmentStart": 1779141134138, + "runtime": 148, + "setupAfterEnvEnd": 1779141134205, + "setupAfterEnvStart": 1779141134158, + "setupFilesEnd": 1779141134141, + "setupFilesStart": 1779141134141, + "slow": false, + "start": 1779141134141 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/application/services/team.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamService", + "create" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService create persists the team with null fallbacks for description and headId", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134255, + "status": "passed", + "title": "persists the team with null fallbacks for description and headId" + }, + { + "ancestorTitles": [ + "TeamService", + "create" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService create verifies the head user exists when headId is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134267, + "status": "passed", + "title": "verifies the head user exists when headId is supplied" + }, + { + "ancestorTitles": [ + "TeamService", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService create skips the head verification when headId is null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134269, + "status": "passed", + "title": "skips the head verification when headId is null" + }, + { + "ancestorTitles": [ + "TeamService", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService search delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134271, + "status": "passed", + "title": "delegates to the repository" + }, + { + "ancestorTitles": [ + "TeamService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService getById returns the team when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134272, + "status": "passed", + "title": "returns the team when found" + }, + { + "ancestorTitles": [ + "TeamService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134273, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "TeamService", + "update" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService update throws NotFoundException when the team does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134275, + "status": "passed", + "title": "throws NotFoundException when the team does not exist" + }, + { + "ancestorTitles": [ + "TeamService", + "update" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService update verifies the head user when headId is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134277, + "status": "passed", + "title": "verifies the head user when headId is supplied" + }, + { + "ancestorTitles": [ + "TeamService", + "update" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService update skips head verification when headId is null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134280, + "status": "passed", + "title": "skips head verification when headId is null" + }, + { + "ancestorTitles": [ + "TeamService", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService update forwards the patch to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134284, + "status": "passed", + "title": "forwards the patch to the repository" + }, + { + "ancestorTitles": [ + "TeamService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService delete throws NotFoundException when the team does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134284, + "status": "passed", + "title": "throws NotFoundException when the team does not exist" + }, + { + "ancestorTitles": [ + "TeamService", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService delete rejects when the team still has members", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134285, + "status": "passed", + "title": "rejects when the team still has members" + }, + { + "ancestorTitles": [ + "TeamService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService delete deletes the team when no members are linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134285, + "status": "passed", + "title": "deletes the team when no members are linked" + }, + { + "ancestorTitles": [ + "TeamService", + "addMember" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService addMember verifies the team and user, then adds the membership without user data by default", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134286, + "status": "passed", + "title": "verifies the team and user, then adds the membership without user data by default" + }, + { + "ancestorTitles": [ + "TeamService", + "addMember" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService addMember attaches the user to the membership when withUser is set", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134286, + "status": "passed", + "title": "attaches the user to the membership when withUser is set" + }, + { + "ancestorTitles": [ + "TeamService", + "removeMember" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService removeMember verifies the team exists before delegating", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134287, + "status": "passed", + "title": "verifies the team exists before delegating" + }, + { + "ancestorTitles": [ + "TeamService", + "listMembers" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService listMembers returns memberships without attached users when withUsers is false", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134287, + "status": "passed", + "title": "returns memberships without attached users when withUsers is false" + }, + { + "ancestorTitles": [ + "TeamService", + "listMembers" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService listMembers attaches users to every membership when withUsers is true", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134287, + "status": "passed", + "title": "attaches users to every membership when withUsers is true" + }, + { + "ancestorTitles": [ + "TeamService", + "listMembers" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamService listMembers falls back to the bare membership when the user lookup fails", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134288, + "status": "passed", + "title": "falls back to the bare membership when the user lookup fails" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134290, + "loadTestEnvironmentEnd": 1779141134137, + "loadTestEnvironmentStart": 1779141134134, + "runtime": 153, + "setupAfterEnvEnd": 1779141134209, + "setupAfterEnvStart": 1779141134165, + "setupFilesEnd": 1779141134137, + "setupFilesStart": 1779141134137, + "slow": false, + "start": 1779141134137 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/cluster-score-analytics.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreAnalyticsHttpMapper", + "toResponse" + ], + "duration": 14, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsHttpMapper toResponse maps the domain into a response, rounding numeric fields to 4dp", + "invocations": 1, + "location": null, + "numPassingAsserts": 12, + "retryReasons": [], + "startAt": 1779141134275, + "status": "passed", + "title": "maps the domain into a response, rounding numeric fields to 4dp" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134290, + "loadTestEnvironmentEnd": 1779141134135, + "loadTestEnvironmentStart": 1779141134132, + "runtime": 155, + "setupAfterEnvEnd": 1779141134198, + "setupAfterEnvStart": 1779141134155, + "setupFilesEnd": 1779141134135, + "setupFilesStart": 1779141134135, + "slow": false, + "start": 1779141134135 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/application/listeners/review-stage-notification.listener.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageNotificationListener" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageNotificationListener triggers the ratee self-assessment email when the review reaches SELF_ASSESSMENT", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134266, + "status": "passed", + "title": "triggers the ratee self-assessment email when the review reaches SELF_ASSESSMENT" + }, + { + "ancestorTitles": [ + "ReviewStageNotificationListener" + ], + "duration": 12, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageNotificationListener triggers the respondents email when the review reaches IN_PROGRESS", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134271, + "status": "passed", + "title": "triggers the respondents email when the review reaches IN_PROGRESS" + }, + { + "ancestorTitles": [ + "ReviewStageNotificationListener" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageNotificationListener triggers the HR report-ready email when the review reaches PROCESSING_BY_HR", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134283, + "status": "passed", + "title": "triggers the HR report-ready email when the review reaches PROCESSING_BY_HR" + }, + { + "ancestorTitles": [ + "ReviewStageNotificationListener" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageNotificationListener triggers the reviewers email when the review reaches PUBLISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134283, + "status": "passed", + "title": "triggers the reviewers email when the review reaches PUBLISHED" + }, + { + "ancestorTitles": [ + "ReviewStageNotificationListener" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageNotificationListener ignores other review stage transitions", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134284, + "status": "passed", + "title": "ignores other review stage transitions" + }, + { + "ancestorTitles": [ + "ReviewStageNotificationListener" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageNotificationListener swallows errors thrown by the notification service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134285, + "status": "passed", + "title": "swallows errors thrown by the notification service" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134290, + "loadTestEnvironmentEnd": 1779141134155, + "loadTestEnvironmentStart": 1779141134154, + "runtime": 134, + "setupAfterEnvEnd": 1779141134233, + "setupAfterEnvStart": 1779141134178, + "setupFilesEnd": 1779141134156, + "setupFilesStart": 1779141134156, + "slow": false, + "start": 1779141134156 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/answer.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "AnswerHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134289, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "AnswerHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerHttpMapper toResponse returns null for missing numerical and text values", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134289, + "status": "passed", + "title": "returns null for missing numerical and text values" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 16, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134352, + "loadTestEnvironmentEnd": 1779141134214, + "loadTestEnvironmentStart": 1779141134213, + "runtime": 138, + "setupAfterEnvEnd": 1779141134269, + "setupAfterEnvStart": 1779141134240, + "setupFilesEnd": 1779141134214, + "setupFilesStart": 1779141134214, + "slow": false, + "start": 1779141134214 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/application/services/question-template.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionTemplateService", + "create" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService create verifies the competence exists and persists the template", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134317, + "status": "passed", + "title": "verifies the competence exists and persists the template" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "create" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService create replaces position relations after creation when positionIds are supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134322, + "status": "passed", + "title": "replaces position relations after creation when positionIds are supplied" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "search" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService search delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134325, + "status": "passed", + "title": "delegates to the repository" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "getById" + ], + "duration": 7, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService getById returns the template when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134328, + "status": "passed", + "title": "returns the template when found" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134335, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService update verifies a new competence id when one is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134336, + "status": "passed", + "title": "verifies a new competence id when one is supplied" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService update replaces position relations when positionIds are supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134337, + "status": "passed", + "title": "replaces position relations when positionIds are supplied" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService update forwards only defined patch fields to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134338, + "status": "passed", + "title": "forwards only defined patch fields to the repository" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService delete rejects when related review questions exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134339, + "status": "passed", + "title": "rejects when related review questions exist" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "delete" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService delete cleans up relations and deletes the template when nothing is linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134339, + "status": "passed", + "title": "cleans up relations and deletes the template when nothing is linked" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "attachPosition / detachPosition / listPositions" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService attachPosition / detachPosition / listPositions attachPosition links the relation and returns updated ids", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134341, + "status": "passed", + "title": "attachPosition links the relation and returns updated ids" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "attachPosition / detachPosition / listPositions" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService attachPosition / detachPosition / listPositions detachPosition unlinks the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134345, + "status": "passed", + "title": "detachPosition unlinks the relation" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "attachPosition / detachPosition / listPositions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService attachPosition / detachPosition / listPositions listPositions maps relations to position ids", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134347, + "status": "passed", + "title": "listPositions maps relations to position ids" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "attachCompetence / detachCompetence / listCompetences" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService attachCompetence / detachCompetence / listCompetences attachCompetence verifies the competence and links the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134348, + "status": "passed", + "title": "attachCompetence verifies the competence and links the relation" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "attachCompetence / detachCompetence / listCompetences" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService attachCompetence / detachCompetence / listCompetences detachCompetence unlinks the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134350, + "status": "passed", + "title": "detachCompetence unlinks the relation" + }, + { + "ancestorTitles": [ + "QuestionTemplateService", + "attachCompetence / detachCompetence / listCompetences" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateService attachCompetence / detachCompetence / listCompetences listCompetences maps relations to competence ids", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134350, + "status": "passed", + "title": "listCompetences maps relations to competence ids" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134361, + "loadTestEnvironmentEnd": 1779141134257, + "loadTestEnvironmentStart": 1779141134255, + "runtime": 104, + "setupAfterEnvEnd": 1779141134307, + "setupAfterEnvStart": 1779141134273, + "setupFilesEnd": 1779141134257, + "setupFilesStart": 1779141134257, + "slow": false, + "start": 1779141134257 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/mappers/report-comment.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportCommentHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134359, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "ReportCommentHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentHttpMapper toResponse returns null sentiment when none is set on the domain", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134359, + "status": "passed", + "title": "returns null sentiment when none is set on the domain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134369, + "loadTestEnvironmentEnd": 1779141134257, + "loadTestEnvironmentStart": 1779141134256, + "runtime": 112, + "setupAfterEnvEnd": 1779141134302, + "setupAfterEnvStart": 1779141134271, + "setupFilesEnd": 1779141134257, + "setupFilesStart": 1779141134257, + "slow": false, + "start": 1779141134257 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/question.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134368, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "QuestionHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionHttpMapper toResponse returns null for missing foreign keys", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134369, + "status": "passed", + "title": "returns null for missing foreign keys" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134373, + "loadTestEnvironmentEnd": 1779141134256, + "loadTestEnvironmentStart": 1779141134254, + "runtime": 117, + "setupAfterEnvEnd": 1779141134302, + "setupAfterEnvStart": 1779141134271, + "setupFilesEnd": 1779141134256, + "setupFilesStart": 1779141134256, + "slow": false, + "start": 1779141134256 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report-insight.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportInsightHttpMapper", + "toResponse" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightHttpMapper toResponse maps every domain field, rounding decimals to 4dp", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134367, + "status": "passed", + "title": "maps every domain field, rounding decimals to 4dp" + }, + { + "ancestorTitles": [ + "StrategicReportInsightHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightHttpMapper toResponse returns null for missing decimal fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134372, + "status": "passed", + "title": "returns null for missing decimal fields" + }, + { + "ancestorTitles": [ + "StrategicReportInsightHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightHttpMapper roundScore rounds a Decimal value to 4 decimal places", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134373, + "status": "passed", + "title": "rounds a Decimal value to 4 decimal places" + }, + { + "ancestorTitles": [ + "StrategicReportInsightHttpMapper", + "roundScore" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightHttpMapper roundScore returns null for null and undefined", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134373, + "status": "passed", + "title": "returns null for null and undefined" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134411, + "loadTestEnvironmentEnd": 1779141134294, + "loadTestEnvironmentStart": 1779141134291, + "runtime": 117, + "setupAfterEnvEnd": 1779141134372, + "setupAfterEnvStart": 1779141134323, + "setupFilesEnd": 1779141134294, + "setupFilesStart": 1779141134294, + "slow": false, + "start": 1779141134294 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/application/services/identity-role.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "IdentityRoleService", + "list" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityRoleService list delegates to the repository getAll", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134409, + "status": "passed", + "title": "delegates to the repository getAll" + }, + { + "ancestorTitles": [ + "IdentityRoleService", + "getByCodes" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "IdentityRoleService getByCodes delegates to the repository findByCodes", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134410, + "status": "passed", + "title": "delegates to the repository findByCodes" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 17, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134431, + "loadTestEnvironmentEnd": 1779141134293, + "loadTestEnvironmentStart": 1779141134291, + "runtime": 138, + "setupAfterEnvEnd": 1779141134372, + "setupAfterEnvStart": 1779141134317, + "setupFilesEnd": 1779141134293, + "setupFilesStart": 1779141134293, + "slow": false, + "start": 1779141134293 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/application/services/competence.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceService", + "create" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService create persists the competence with null fallbacks for code and description", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134412, + "status": "passed", + "title": "persists the competence with null fallbacks for code and description" + }, + { + "ancestorTitles": [ + "CompetenceService", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService create passes through optional fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134416, + "status": "passed", + "title": "passes through optional fields" + }, + { + "ancestorTitles": [ + "CompetenceService", + "search" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService search delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134417, + "status": "passed", + "title": "delegates to the repository" + }, + { + "ancestorTitles": [ + "CompetenceService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService getById returns the competence when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134417, + "status": "passed", + "title": "returns the competence when found" + }, + { + "ancestorTitles": [ + "CompetenceService", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134418, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "CompetenceService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService update throws NotFoundException when the competence does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134418, + "status": "passed", + "title": "throws NotFoundException when the competence does not exist" + }, + { + "ancestorTitles": [ + "CompetenceService", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService update forwards only defined patch fields to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134419, + "status": "passed", + "title": "forwards only defined patch fields to the repository" + }, + { + "ancestorTitles": [ + "CompetenceService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService delete rejects when related question templates exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134419, + "status": "passed", + "title": "rejects when related question templates exist" + }, + { + "ancestorTitles": [ + "CompetenceService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService delete rejects when related positions exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134421, + "status": "passed", + "title": "rejects when related positions exist" + }, + { + "ancestorTitles": [ + "CompetenceService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService delete cleans up relations and deletes the competence when nothing is linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134422, + "status": "passed", + "title": "cleans up relations and deletes the competence when nothing is linked" + }, + { + "ancestorTitles": [ + "CompetenceService", + "attachPosition / detachPosition / listPositions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService attachPosition / detachPosition / listPositions attachPosition links the relation and returns updated position ids", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134423, + "status": "passed", + "title": "attachPosition links the relation and returns updated position ids" + }, + { + "ancestorTitles": [ + "CompetenceService", + "attachPosition / detachPosition / listPositions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService attachPosition / detachPosition / listPositions detachPosition unlinks the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134424, + "status": "passed", + "title": "detachPosition unlinks the relation" + }, + { + "ancestorTitles": [ + "CompetenceService", + "attachPosition / detachPosition / listPositions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService attachPosition / detachPosition / listPositions listPositions maps relations to position ids", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134425, + "status": "passed", + "title": "listPositions maps relations to position ids" + }, + { + "ancestorTitles": [ + "CompetenceService", + "attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates throws NotFoundException when the question template does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134426, + "status": "passed", + "title": "throws NotFoundException when the question template does not exist" + }, + { + "ancestorTitles": [ + "CompetenceService", + "attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates attachQuestionTemplate links and returns the updated ids", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134428, + "status": "passed", + "title": "attachQuestionTemplate links and returns the updated ids" + }, + { + "ancestorTitles": [ + "CompetenceService", + "attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates detachQuestionTemplate unlinks the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134429, + "status": "passed", + "title": "detachQuestionTemplate unlinks the relation" + }, + { + "ancestorTitles": [ + "CompetenceService", + "attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceService attachQuestionTemplate / detachQuestionTemplate / listQuestionTemplates listQuestionTemplates maps relations to question template ids", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134430, + "status": "passed", + "title": "listQuestionTemplates maps relations to question template ids" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134440, + "loadTestEnvironmentEnd": 1779141134273, + "loadTestEnvironmentStart": 1779141134270, + "runtime": 167, + "setupAfterEnvEnd": 1779141134348, + "setupAfterEnvStart": 1779141134300, + "setupFilesEnd": 1779141134273, + "setupFilesStart": 1779141134273, + "slow": false, + "start": 1779141134273 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/listeners/review-stage.listener.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageListener" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener still emits review.stage.processed when the new stage is not FINISHED", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134422, + "status": "passed", + "title": "still emits review.stage.processed when the new stage is not FINISHED" + }, + { + "ancestorTitles": [ + "ReviewStageListener" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener warns and emits processed when the review is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134425, + "status": "passed", + "title": "warns and emits processed when the review is missing" + }, + { + "ancestorTitles": [ + "ReviewStageListener" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener skips cycle completion when the review has no cycle attached", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134426, + "status": "passed", + "title": "skips cycle completion when the review has no cycle attached" + }, + { + "ancestorTitles": [ + "ReviewStageListener" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener completes the cycle when all remaining reviews are past response collection", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134428, + "status": "passed", + "title": "completes the cycle when all remaining reviews are past response collection" + }, + { + "ancestorTitles": [ + "ReviewStageListener" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener does not complete the cycle while another review is still active", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134430, + "status": "passed", + "title": "does not complete the cycle while another review is still active" + }, + { + "ancestorTitles": [ + "ReviewStageListener" + ], + "duration": 9, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageListener still emits review.stage.processed when completeCycle fails", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134431, + "status": "passed", + "title": "still emits review.stage.processed when completeCycle fails" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134440, + "loadTestEnvironmentEnd": 1779141134294, + "loadTestEnvironmentStart": 1779141134292, + "runtime": 146, + "setupAfterEnvEnd": 1779141134375, + "setupAfterEnvStart": 1779141134310, + "setupFilesEnd": 1779141134294, + "setupFilesStart": 1779141134294, + "slow": false, + "start": 1779141134294 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/review.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewHttpMapper", + "toResponse" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 12, + "retryReasons": [], + "startAt": 1779141134436, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "ReviewHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewHttpMapper toResponse emits explicit null for missing optional relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134438, + "status": "passed", + "title": "emits explicit null for missing optional relations" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134443, + "loadTestEnvironmentEnd": 1779141134300, + "loadTestEnvironmentStart": 1779141134297, + "runtime": 143, + "setupAfterEnvEnd": 1779141134357, + "setupAfterEnvStart": 1779141134317, + "setupFilesEnd": 1779141134300, + "setupFilesStart": 1779141134300, + "slow": false, + "start": 1779141134300 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/mappers/report.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportHttpMapper", + "toResponse" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportHttpMapper toResponse produces summary/totals/insights blocks split by entity type", + "invocations": 1, + "location": null, + "numPassingAsserts": 14, + "retryReasons": [], + "startAt": 1779141134438, + "status": "passed", + "title": "produces summary/totals/insights blocks split by entity type" + }, + { + "ancestorTitles": [ + "ReportHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportHttpMapper toResponse returns null totals blocks when every summary field is null", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134441, + "status": "passed", + "title": "returns null totals blocks when every summary field is null" + }, + { + "ancestorTitles": [ + "ReportHttpMapper", + "toTextAnswerResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportHttpMapper toTextAnswerResponse maps a ReportTextAnswerDto onto a TextAnswerResponse", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134442, + "status": "passed", + "title": "maps a ReportTextAnswerDto onto a TextAnswerResponse" + }, + { + "ancestorTitles": [ + "ReportHttpMapper", + "toTextAnswerResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportHttpMapper toTextAnswerResponse returns null questionTitle when none is provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134442, + "status": "passed", + "title": "returns null questionTitle when none is provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134496, + "loadTestEnvironmentEnd": 1779141134365, + "loadTestEnvironmentStart": 1779141134363, + "runtime": 131, + "setupAfterEnvEnd": 1779141134430, + "setupAfterEnvStart": 1779141134391, + "setupFilesEnd": 1779141134365, + "setupFilesStart": 1779141134365, + "slow": false, + "start": 1779141134365 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/presentation/mappers/position.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134496, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "PositionHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHttpMapper toResponse passes null description through to the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134496, + "status": "passed", + "title": "passes null description through to the response" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134502, + "loadTestEnvironmentEnd": 1779141134357, + "loadTestEnvironmentStart": 1779141134354, + "runtime": 145, + "setupAfterEnvEnd": 1779141134427, + "setupAfterEnvStart": 1779141134386, + "setupFilesEnd": 1779141134357, + "setupFilesStart": 1779141134357, + "slow": false, + "start": 1779141134357 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/presentation/mappers/strategic-report.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportHttpMapper", + "toResponse" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportHttpMapper toResponse maps every domain field onto the response and includes relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 13, + "retryReasons": [], + "startAt": 1779141134497, + "status": "passed", + "title": "maps every domain field onto the response and includes relations" + }, + { + "ancestorTitles": [ + "StrategicReportHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportHttpMapper toResponse returns empty analytics/insights arrays when none are provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134500, + "status": "passed", + "title": "returns empty analytics/insights arrays when none are provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 12, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134511, + "loadTestEnvironmentEnd": 1779141134392, + "loadTestEnvironmentStart": 1779141134391, + "runtime": 119, + "setupAfterEnvEnd": 1779141134461, + "setupAfterEnvStart": 1779141134410, + "setupFilesEnd": 1779141134392, + "setupFilesStart": 1779141134392, + "slow": false, + "start": 1779141134392 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/application/services/cluster.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterService", + "create" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService create rejects when lowerBound > upperBound", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134499, + "status": "passed", + "title": "rejects when lowerBound > upperBound" + }, + { + "ancestorTitles": [ + "ClusterService", + "create" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService create verifies the competence exists before persisting", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134502, + "status": "passed", + "title": "verifies the competence exists before persisting" + }, + { + "ancestorTitles": [ + "ClusterService", + "search" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService search delegates to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134504, + "status": "passed", + "title": "delegates to the repository" + }, + { + "ancestorTitles": [ + "ClusterService", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService getById returns the cluster when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134505, + "status": "passed", + "title": "returns the cluster when found" + }, + { + "ancestorTitles": [ + "ClusterService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134505, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "ClusterService", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService update throws NotFoundException when the cluster does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134506, + "status": "passed", + "title": "throws NotFoundException when the cluster does not exist" + }, + { + "ancestorTitles": [ + "ClusterService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService update verifies a new competence id before updating", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134506, + "status": "passed", + "title": "verifies a new competence id before updating" + }, + { + "ancestorTitles": [ + "ClusterService", + "update" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService update skips competence verification when the id does not change", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134507, + "status": "passed", + "title": "skips competence verification when the id does not change" + }, + { + "ancestorTitles": [ + "ClusterService", + "update" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService update validates merged bounds against current values", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134509, + "status": "passed", + "title": "validates merged bounds against current values" + }, + { + "ancestorTitles": [ + "ClusterService", + "update" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService update forwards only defined patch fields to the repository", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134509, + "status": "passed", + "title": "forwards only defined patch fields to the repository" + }, + { + "ancestorTitles": [ + "ClusterService", + "delete" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService delete throws when the cluster does not exist", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134510, + "status": "passed", + "title": "throws when the cluster does not exist" + }, + { + "ancestorTitles": [ + "ClusterService", + "delete" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterService delete deletes when found", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134510, + "status": "passed", + "title": "deletes when found" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134514, + "loadTestEnvironmentEnd": 1779141134414, + "loadTestEnvironmentStart": 1779141134413, + "runtime": 100, + "setupAfterEnvEnd": 1779141134478, + "setupAfterEnvStart": 1779141134432, + "setupFilesEnd": 1779141134414, + "setupFilesStart": 1779141134414, + "slow": false, + "start": 1779141134414 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/services/report-insight.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportInsightService", + "getByReportId" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService getByReportId returns insights after the access check passes", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134507, + "status": "passed", + "title": "returns insights after the access check passes" + }, + { + "ancestorTitles": [ + "ReportInsightService", + "getByReportId" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService getByReportId throws NotFoundException when the repo returns null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134511, + "status": "passed", + "title": "throws NotFoundException when the repo returns null" + }, + { + "ancestorTitles": [ + "ReportInsightService", + "getById" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService getById throws NotFoundException when the insight is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134512, + "status": "passed", + "title": "throws NotFoundException when the insight is missing" + }, + { + "ancestorTitles": [ + "ReportInsightService", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightService getById re-checks access via the parent report", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134513, + "status": "passed", + "title": "re-checks access via the parent report" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134516, + "loadTestEnvironmentEnd": 1779141134376, + "loadTestEnvironmentStart": 1779141134373, + "runtime": 140, + "setupAfterEnvEnd": 1779141134455, + "setupAfterEnvStart": 1779141134405, + "setupFilesEnd": 1779141134376, + "setupFilesStart": 1779141134376, + "slow": false, + "start": 1779141134376 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/presentation/mappers/competence.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceHttpMapper", + "toResponse" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134513, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "CompetenceHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceHttpMapper toResponse passes null code/description through to the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134515, + "status": "passed", + "title": "passes null code/description through to the response" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134575, + "loadTestEnvironmentEnd": 1779141134459, + "loadTestEnvironmentStart": 1779141134455, + "runtime": 116, + "setupAfterEnvEnd": 1779141134529, + "setupAfterEnvStart": 1779141134488, + "setupFilesEnd": 1779141134459, + "setupFilesStart": 1779141134459, + "slow": false, + "start": 1779141134459 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/services/text-answer.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TextAnswerService", + "listByReview" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService listByReview returns text answers paired with their question titles for admin/HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134567, + "status": "passed", + "title": "returns text answers paired with their question titles for admin/HR actors" + }, + { + "ancestorTitles": [ + "TextAnswerService", + "listByReview" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService listByReview skips answers with empty or whitespace-only text", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134571, + "status": "passed", + "title": "skips answers with empty or whitespace-only text" + }, + { + "ancestorTitles": [ + "TextAnswerService", + "listByReview" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService listByReview throws ForbiddenException for non-admin/HR actors", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134573, + "status": "passed", + "title": "throws ForbiddenException for non-admin/HR actors" + }, + { + "ancestorTitles": [ + "TextAnswerService", + "listByReview" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TextAnswerService listByReview skips the access check when no actor is supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134574, + "status": "passed", + "title": "skips the access check when no actor is supplied" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134576, + "loadTestEnvironmentEnd": 1779141134448, + "loadTestEnvironmentStart": 1779141134445, + "runtime": 128, + "setupAfterEnvEnd": 1779141134507, + "setupAfterEnvStart": 1779141134479, + "setupFilesEnd": 1779141134448, + "setupFilesStart": 1779141134448, + "slow": false, + "start": 1779141134448 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/presentation/mappers/team-membership.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamMembershipHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMembershipHttpMapper toResponse maps the basic membership without the user when none is set", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134575, + "status": "passed", + "title": "maps the basic membership without the user when none is set" + }, + { + "ancestorTitles": [ + "TeamMembershipHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMembershipHttpMapper toResponse includes the user when the domain carries one", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134576, + "status": "passed", + "title": "includes the user when the domain carries one" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134576, + "loadTestEnvironmentEnd": 1779141134447, + "loadTestEnvironmentStart": 1779141134444, + "runtime": 129, + "setupAfterEnvEnd": 1779141134523, + "setupAfterEnvStart": 1779141134479, + "setupFilesEnd": 1779141134447, + "setupFilesStart": 1779141134447, + "slow": false, + "start": 1779141134447 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/services/strategic-report-analytics.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportAnalyticsService", + "getByStrategicReportId" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService getByStrategicReportId returns analytics after the access check passes", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134573, + "status": "passed", + "title": "returns analytics after the access check passes" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsService", + "getByStrategicReportId" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService getByStrategicReportId throws NotFoundException when the repo returns null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134575, + "status": "passed", + "title": "throws NotFoundException when the repo returns null" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsService", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134576, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsService", + "getById" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsService getById re-checks access via the parent strategic report", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134576, + "status": "passed", + "title": "re-checks access via the parent strategic report" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134579, + "loadTestEnvironmentEnd": 1779141134449, + "loadTestEnvironmentStart": 1779141134446, + "runtime": 130, + "setupAfterEnvEnd": 1779141134502, + "setupAfterEnvStart": 1779141134470, + "setupFilesEnd": 1779141134449, + "setupFilesStart": 1779141134449, + "slow": false, + "start": 1779141134449 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/presentation/mappers/team.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134578, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "TeamHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamHttpMapper toResponse passes null description and headId through to the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134578, + "status": "passed", + "title": "passes null description and headId through to the response" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 10, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134601, + "loadTestEnvironmentEnd": 1779141134522, + "loadTestEnvironmentStart": 1779141134520, + "runtime": 79, + "setupAfterEnvEnd": 1779141134593, + "setupAfterEnvStart": 1779141134547, + "setupFilesEnd": 1779141134522, + "setupFilesStart": 1779141134522, + "slow": false, + "start": 1779141134522 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/constants/cycle-stage-transitions.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS allows NEW to move to ACTIVE or CANCELED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134596, + "status": "passed", + "title": "allows NEW to move to ACTIVE or CANCELED only" + }, + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS allows ACTIVE to move to FINISHED or CANCELED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134597, + "status": "passed", + "title": "allows ACTIVE to move to FINISHED or CANCELED only" + }, + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS allows FINISHED to move to PREPARING_REPORT or ARCHIVED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134597, + "status": "passed", + "title": "allows FINISHED to move to PREPARING_REPORT or ARCHIVED only" + }, + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS allows PREPARING_REPORT to move to PUBLISHED or ARCHIVED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134597, + "status": "passed", + "title": "allows PREPARING_REPORT to move to PUBLISHED or ARCHIVED only" + }, + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS allows PUBLISHED to move to ARCHIVED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134598, + "status": "passed", + "title": "allows PUBLISHED to move to ARCHIVED only" + }, + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS allows CANCELED to move to ARCHIVED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134598, + "status": "passed", + "title": "allows CANCELED to move to ARCHIVED only" + }, + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS treats ARCHIVED as a terminal stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134598, + "status": "passed", + "title": "treats ARCHIVED as a terminal stage" + }, + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS covers every CycleStage as an entry key", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134598, + "status": "passed", + "title": "covers every CycleStage as an entry key" + }, + { + "ancestorTitles": [ + "CYCLE_STAGE_TRANSITIONS" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CYCLE_STAGE_TRANSITIONS never lists a stage as a transition from itself", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134599, + "status": "passed", + "title": "never lists a stage as a transition from itself" + }, + { + "ancestorTitles": [ + "TRANSITION_REASONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TRANSITION_REASONS exposes the canonical reason strings", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134600, + "status": "passed", + "title": "exposes the canonical reason strings" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134606, + "loadTestEnvironmentEnd": 1779141134538, + "loadTestEnvironmentStart": 1779141134536, + "runtime": 68, + "setupAfterEnvEnd": 1779141134596, + "setupAfterEnvStart": 1779141134561, + "setupFilesEnd": 1779141134538, + "setupFilesStart": 1779141134538, + "slow": false, + "start": 1779141134538 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cluster-score.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreMapper toDomain converts a prisma row into a ClusterScoreDomain instance", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134603, + "status": "passed", + "title": "converts a prisma row into a ClusterScoreDomain instance" + }, + { + "ancestorTitles": [ + "ClusterScoreMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreMapper toPrisma serialises a domain into a Prisma create input and rounds the score to 4dp", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134604, + "status": "passed", + "title": "serialises a domain into a Prisma create input and rounds the score to 4dp" + }, + { + "ancestorTitles": [ + "ClusterScoreMapper", + "toDomainWithRelations" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreMapper toDomainWithRelations returns the full aggregate including cluster and ratee", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134604, + "status": "passed", + "title": "returns the full aggregate including cluster and ratee" + }, + { + "ancestorTitles": [ + "ClusterScoreMapper", + "toScoreDecimalString" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreMapper toScoreDecimalString rounds to exactly 4 decimal places", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134605, + "status": "passed", + "title": "rounds to exactly 4 decimal places" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134611, + "loadTestEnvironmentEnd": 1779141134507, + "loadTestEnvironmentStart": 1779141134504, + "runtime": 103, + "setupAfterEnvEnd": 1779141134574, + "setupAfterEnvStart": 1779141134536, + "setupFilesEnd": 1779141134508, + "setupFilesStart": 1779141134508, + "slow": false, + "start": 1779141134508 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/review-question-relation.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewQuestionRelationHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 10, + "retryReasons": [], + "startAt": 1779141134610, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationHttpMapper toResponse returns null for missing competence code/description", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134611, + "status": "passed", + "title": "returns null for missing competence code/description" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134626, + "loadTestEnvironmentEnd": 1779141134513, + "loadTestEnvironmentStart": 1779141134512, + "runtime": 113, + "setupAfterEnvEnd": 1779141134573, + "setupAfterEnvStart": 1779141134539, + "setupFilesEnd": 1779141134513, + "setupFilesStart": 1779141134513, + "slow": false, + "start": 1779141134513 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/presentation/mappers/cluster.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134625, + "status": "passed", + "title": "maps every domain field onto the response" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134645, + "loadTestEnvironmentEnd": 1779141134582, + "loadTestEnvironmentStart": 1779141134581, + "runtime": 63, + "setupAfterEnvEnd": 1779141134634, + "setupAfterEnvStart": 1779141134598, + "setupFilesEnd": 1779141134582, + "setupFilesStart": 1779141134582, + "slow": false, + "start": 1779141134582 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/cluster-score-with-relations.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreWithRelationsDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreWithRelationsDomain create builds the aggregate with cluster, ratee, and score relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 10, + "retryReasons": [], + "startAt": 1779141134643, + "status": "passed", + "title": "builds the aggregate with cluster, ratee, and score relations" + }, + { + "ancestorTitles": [ + "ClusterScoreWithRelationsDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreWithRelationsDomain create defaults the cycleId to null when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134644, + "status": "passed", + "title": "defaults the cycleId to null when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134652, + "loadTestEnvironmentEnd": 1779141134579, + "loadTestEnvironmentStart": 1779141134577, + "runtime": 72, + "setupAfterEnvEnd": 1779141134640, + "setupAfterEnvStart": 1779141134596, + "setupFilesEnd": 1779141134580, + "setupFilesStart": 1779141134580, + "slow": false, + "start": 1779141134580 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cluster-score-analytics.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreAnalyticsMapper", + "toDomain" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsMapper toDomain converts the row to a ClusterScoreAnalyticsDomain with Decimal values", + "invocations": 1, + "location": null, + "numPassingAsserts": 11, + "retryReasons": [], + "startAt": 1779141134649, + "status": "passed", + "title": "converts the row to a ClusterScoreAnalyticsDomain with Decimal values" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsMapper", + "toPrisma" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsMapper toPrisma renders all numeric fields as 4-decimal-place strings", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134651, + "status": "passed", + "title": "renders all numeric fields as 4-decimal-place strings" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134661, + "loadTestEnvironmentEnd": 1779141134506, + "loadTestEnvironmentStart": 1779141134504, + "runtime": 154, + "setupAfterEnvEnd": 1779141134588, + "setupAfterEnvStart": 1779141134547, + "setupFilesEnd": 1779141134507, + "setupFilesStart": 1779141134507, + "slow": false, + "start": 1779141134507 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/application/listeners/user-created-notification.listener.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "UserCreatedNotificationListener" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserCreatedNotificationListener forwards user.created events to the welcome notification", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134656, + "status": "passed", + "title": "forwards user.created events to the welcome notification" + }, + { + "ancestorTitles": [ + "UserCreatedNotificationListener" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserCreatedNotificationListener swallows errors thrown by the notification service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134658, + "status": "passed", + "title": "swallows errors thrown by the notification service" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134668, + "loadTestEnvironmentEnd": 1779141134612, + "loadTestEnvironmentStart": 1779141134610, + "runtime": 56, + "setupAfterEnvEnd": 1779141134661, + "setupAfterEnvStart": 1779141134634, + "setupFilesEnd": 1779141134612, + "setupFilesStart": 1779141134612, + "slow": false, + "start": 1779141134612 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/domain/strategic-report-analytics.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportAnalyticsDomain", + "create" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsDomain create creates analytics with every decimal field coerced", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134664, + "status": "passed", + "title": "creates analytics with every decimal field coerced" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsDomain create defaults missing decimal fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134667, + "status": "passed", + "title": "defaults missing decimal fields to null" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134670, + "loadTestEnvironmentEnd": 1779141134614, + "loadTestEnvironmentStart": 1779141134613, + "runtime": 55, + "setupAfterEnvEnd": 1779141134668, + "setupAfterEnvStart": 1779141134636, + "setupFilesEnd": 1779141134615, + "setupFilesStart": 1779141134615, + "slow": false, + "start": 1779141134615 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/domain/team.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamDomain create creates a team with every supplied property", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134669, + "status": "passed", + "title": "creates a team with every supplied property" + }, + { + "ancestorTitles": [ + "TeamDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamDomain create defaults description and headId to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134670, + "status": "passed", + "title": "defaults description and headId to null" + }, + { + "ancestorTitles": [ + "TeamDomain", + "withPatch" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamDomain withPatch returns a new instance carrying the supplied patch", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134670, + "status": "passed", + "title": "returns a new instance carrying the supplied patch" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134670, + "loadTestEnvironmentEnd": 1779141134612, + "loadTestEnvironmentStart": 1779141134609, + "runtime": 58, + "setupAfterEnvEnd": 1779141134666, + "setupAfterEnvStart": 1779141134640, + "setupFilesEnd": 1779141134612, + "setupFilesStart": 1779141134612, + "slow": false, + "start": 1779141134612 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/constants/response-status-transitions.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RESPONSE_STATUS_TRANSITIONS" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RESPONSE_STATUS_TRANSITIONS allows PENDING to move to IN_PROGRESS or CANCELED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134669, + "status": "passed", + "title": "allows PENDING to move to IN_PROGRESS or CANCELED" + }, + { + "ancestorTitles": [ + "RESPONSE_STATUS_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RESPONSE_STATUS_TRANSITIONS allows IN_PROGRESS to move to COMPLETED or CANCELED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134670, + "status": "passed", + "title": "allows IN_PROGRESS to move to COMPLETED or CANCELED" + }, + { + "ancestorTitles": [ + "RESPONSE_STATUS_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RESPONSE_STATUS_TRANSITIONS treats COMPLETED and CANCELED as terminal", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134670, + "status": "passed", + "title": "treats COMPLETED and CANCELED as terminal" + }, + { + "ancestorTitles": [ + "RESPONSE_STATUS_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RESPONSE_STATUS_TRANSITIONS covers every ResponseStatus as an entry key", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134670, + "status": "passed", + "title": "covers every ResponseStatus as an entry key" + }, + { + "ancestorTitles": [ + "TRANSITION_REASONS (response)" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TRANSITION_REASONS (response) exposes the canonical reason strings", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134670, + "status": "passed", + "title": "exposes the canonical reason strings" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134681, + "loadTestEnvironmentEnd": 1779141134579, + "loadTestEnvironmentStart": 1779141134578, + "runtime": 102, + "setupAfterEnvEnd": 1779141134617, + "setupAfterEnvStart": 1779141134595, + "setupFilesEnd": 1779141134579, + "setupFilesStart": 1779141134579, + "slow": false, + "start": 1779141134579 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/presentation/mappers/respondent.http.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RespondentHttpMapper", + "toResponse" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentHttpMapper toResponse maps every domain field onto the response", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134680, + "status": "passed", + "title": "maps every domain field onto the response" + }, + { + "ancestorTitles": [ + "RespondentHttpMapper", + "toResponse" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentHttpMapper toResponse returns null for missing optional fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134680, + "status": "passed", + "title": "returns null for missing optional fields" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134692, + "loadTestEnvironmentEnd": 1779141134579, + "loadTestEnvironmentStart": 1779141134577, + "runtime": 112, + "setupAfterEnvEnd": 1779141134636, + "setupAfterEnvStart": 1779141134596, + "setupFilesEnd": 1779141134580, + "setupFilesStart": 1779141134580, + "slow": false, + "start": 1779141134580 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/application/services/strategic-report-insight.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StartegicReportInsightService", + "getByStrategicReportId" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService getByStrategicReportId returns insights after the access check passes", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134681, + "status": "passed", + "title": "returns insights after the access check passes" + }, + { + "ancestorTitles": [ + "StartegicReportInsightService", + "getByStrategicReportId" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService getByStrategicReportId throws NotFoundException when the repo returns null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134685, + "status": "passed", + "title": "throws NotFoundException when the repo returns null" + }, + { + "ancestorTitles": [ + "StartegicReportInsightService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService getById throws NotFoundException when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134688, + "status": "passed", + "title": "throws NotFoundException when missing" + }, + { + "ancestorTitles": [ + "StartegicReportInsightService", + "getById" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StartegicReportInsightService getById re-checks access via the parent strategic report", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134690, + "status": "passed", + "title": "re-checks access via the parent strategic report" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134703, + "loadTestEnvironmentEnd": 1779141134656, + "loadTestEnvironmentStart": 1779141134654, + "runtime": 47, + "setupAfterEnvEnd": 1779141134697, + "setupAfterEnvStart": 1779141134672, + "setupFilesEnd": 1779141134656, + "setupFilesStart": 1779141134656, + "slow": false, + "start": 1779141134656 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-comment.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportCommentMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentMapper toDomain converts a prisma row into a ReportCommentDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134702, + "status": "passed", + "title": "converts a prisma row into a ReportCommentDomain" + }, + { + "ancestorTitles": [ + "ReportCommentMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentMapper toDomain falls back commentSentiment to null when prisma returns null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134702, + "status": "passed", + "title": "falls back commentSentiment to null when prisma returns null" + }, + { + "ancestorTitles": [ + "ReportCommentMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentMapper toPrisma serialises a domain object into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134703, + "status": "passed", + "title": "serialises a domain object into a Prisma create input" + }, + { + "ancestorTitles": [ + "ReportCommentMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentMapper toPrisma returns undefined sentiment when domain sentiment is null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134703, + "status": "passed", + "title": "returns undefined sentiment when domain sentiment is null" + }, + { + "ancestorTitles": [ + "ReportCommentMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentMapper enum conversions uppercases respondentCategories in toDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134703, + "status": "passed", + "title": "uppercases respondentCategories in toDomain" + }, + { + "ancestorTitles": [ + "ReportCommentMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentMapper enum conversions uppercases sentiment in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134703, + "status": "passed", + "title": "uppercases sentiment in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134715, + "loadTestEnvironmentEnd": 1779141134638, + "loadTestEnvironmentStart": 1779141134637, + "runtime": 76, + "setupAfterEnvEnd": 1779141134700, + "setupAfterEnvStart": 1779141134663, + "setupFilesEnd": 1779141134639, + "setupFilesStart": 1779141134639, + "slow": false, + "start": 1779141134639 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/infrastructure/mappers/question-template.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionTemplateMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateMapper toDomain converts a prisma row into a QuestionTemplateDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134712, + "status": "passed", + "title": "converts a prisma row into a QuestionTemplateDomain" + }, + { + "ancestorTitles": [ + "QuestionTemplateMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateMapper toDomain coerces null isForSelfassessment to false", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134713, + "status": "passed", + "title": "coerces null isForSelfassessment to false" + }, + { + "ancestorTitles": [ + "QuestionTemplateMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateMapper toDomain reads positionIds from positionRelations when supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134714, + "status": "passed", + "title": "reads positionIds from positionRelations when supplied" + }, + { + "ancestorTitles": [ + "QuestionTemplateMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateMapper toPrisma serialises a domain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134714, + "status": "passed", + "title": "serialises a domain into a Prisma create input" + }, + { + "ancestorTitles": [ + "QuestionTemplateMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateMapper enum conversions uppercases the answer type in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134715, + "status": "passed", + "title": "uppercases the answer type in both directions" + }, + { + "ancestorTitles": [ + "QuestionTemplateMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateMapper enum conversions uppercases the status in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134715, + "status": "passed", + "title": "uppercases the status in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134719, + "loadTestEnvironmentEnd": 1779141134671, + "loadTestEnvironmentStart": 1779141134670, + "runtime": 48, + "setupAfterEnvEnd": 1779141134707, + "setupAfterEnvStart": 1779141134683, + "setupFilesEnd": 1779141134671, + "setupFilesStart": 1779141134671, + "slow": false, + "start": 1779141134671 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportMapper toDomain converts a prisma row into a ReportDomain with empty relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 11, + "retryReasons": [], + "startAt": 1779141134717, + "status": "passed", + "title": "converts a prisma row into a ReportDomain with empty relations" + }, + { + "ancestorTitles": [ + "ReportMapper", + "toDomainWithRelations" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportMapper toDomainWithRelations includes analytics, comments, and insights in the aggregate", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134717, + "status": "passed", + "title": "includes analytics, comments, and insights in the aggregate" + }, + { + "ancestorTitles": [ + "ReportMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportMapper toPrisma serialises a domain object into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134718, + "status": "passed", + "title": "serialises a domain object into a Prisma create input" + }, + { + "ancestorTitles": [ + "ReportMapper", + "respondent category conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportMapper respondent category conversions uppercases in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134718, + "status": "passed", + "title": "uppercases in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134720, + "loadTestEnvironmentEnd": 1779141134678, + "loadTestEnvironmentStart": 1779141134677, + "runtime": 42, + "setupAfterEnvEnd": 1779141134716, + "setupAfterEnvStart": 1779141134689, + "setupFilesEnd": 1779141134678, + "setupFilesStart": 1779141134678, + "slow": false, + "start": 1779141134678 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/respondent.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RespondentDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentDomain create creates a respondent with all supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 13, + "retryReasons": [], + "startAt": 1779141134719, + "status": "passed", + "title": "creates a respondent with all supplied properties" + }, + { + "ancestorTitles": [ + "RespondentDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentDomain create defaults responseStatus to PENDING when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134720, + "status": "passed", + "title": "defaults responseStatus to PENDING when not provided" + }, + { + "ancestorTitles": [ + "RespondentDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentDomain create normalises missing optional fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134720, + "status": "passed", + "title": "normalises missing optional fields to null" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134721, + "loadTestEnvironmentEnd": 1779141134679, + "loadTestEnvironmentStart": 1779141134677, + "runtime": 42, + "setupAfterEnvEnd": 1779141134716, + "setupAfterEnvStart": 1779141134691, + "setupFilesEnd": 1779141134679, + "setupFilesStart": 1779141134679, + "slow": false, + "start": 1779141134679 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cycle.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleMapper toDomain converts a prisma row into a CycleDomain instance", + "invocations": 1, + "location": null, + "numPassingAsserts": 10, + "retryReasons": [], + "startAt": 1779141134720, + "status": "passed", + "title": "converts a prisma row into a CycleDomain instance" + }, + { + "ancestorTitles": [ + "CycleMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleMapper toDomain treats null isActive as true", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134721, + "status": "passed", + "title": "treats null isActive as true" + }, + { + "ancestorTitles": [ + "CycleMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleMapper toPrisma serialises a CycleDomain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134721, + "status": "passed", + "title": "serialises a CycleDomain into a Prisma create input" + }, + { + "ancestorTitles": [ + "CycleMapper", + "stage conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleMapper stage conversions uppercases the stage when going to Prisma", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134721, + "status": "passed", + "title": "uppercases the stage when going to Prisma" + }, + { + "ancestorTitles": [ + "CycleMapper", + "stage conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleMapper stage conversions uppercases the stage when coming from Prisma", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134721, + "status": "passed", + "title": "uppercases the stage when coming from Prisma" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134724, + "loadTestEnvironmentEnd": 1779141134665, + "loadTestEnvironmentStart": 1779141134663, + "runtime": 59, + "setupAfterEnvEnd": 1779141134713, + "setupAfterEnvStart": 1779141134676, + "setupFilesEnd": 1779141134665, + "setupFilesStart": 1779141134665, + "slow": false, + "start": 1779141134665 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-insight.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportInsightMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightMapper toDomain converts a prisma row into a ReportInsightDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134719, + "status": "passed", + "title": "converts a prisma row into a ReportInsightDomain" + }, + { + "ancestorTitles": [ + "ReportInsightMapper", + "toPrisma" + ], + "duration": 3, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightMapper toPrisma serialises a domain object into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134720, + "status": "passed", + "title": "serialises a domain object into a Prisma create input" + }, + { + "ancestorTitles": [ + "ReportInsightMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightMapper enum conversions uppercases the entity type in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134723, + "status": "passed", + "title": "uppercases the entity type in both directions" + }, + { + "ancestorTitles": [ + "ReportInsightMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightMapper enum conversions uppercases the insight type in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134723, + "status": "passed", + "title": "uppercases the insight type in both directions" + }, + { + "ancestorTitles": [ + "ReportInsightMapper", + "toDecimalString" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightMapper toDecimalString returns undefined for null/undefined inputs", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134723, + "status": "passed", + "title": "returns undefined for null/undefined inputs" + }, + { + "ancestorTitles": [ + "ReportInsightMapper", + "toDecimalString" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightMapper toDecimalString rounds to 4 decimal places", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134723, + "status": "passed", + "title": "rounds to 4 decimal places" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134748, + "loadTestEnvironmentEnd": 1779141134684, + "loadTestEnvironmentStart": 1779141134683, + "runtime": 64, + "setupAfterEnvEnd": 1779141134743, + "setupAfterEnvStart": 1779141134709, + "setupFilesEnd": 1779141134684, + "setupFilesStart": 1779141134684, + "slow": false, + "start": 1779141134684 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/cycle-stage-history.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageHistoryMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryMapper toDomain converts a prisma row into a CycleStageHistoryDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134747, + "status": "passed", + "title": "converts a prisma row into a CycleStageHistoryDomain" + }, + { + "ancestorTitles": [ + "CycleStageHistoryMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryMapper toPrisma serialises a domain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134748, + "status": "passed", + "title": "serialises a domain into a Prisma create input" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134760, + "loadTestEnvironmentEnd": 1779141134650, + "loadTestEnvironmentStart": 1779141134649, + "runtime": 110, + "setupAfterEnvEnd": 1779141134722, + "setupAfterEnvStart": 1779141134668, + "setupFilesEnd": 1779141134650, + "setupFilesStart": 1779141134650, + "slow": false, + "start": 1779141134650 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/application/services/position-hierarchy.service.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionHierarchyService", + "link" + ], + "duration": 4, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService link rejects when superior and subordinate are the same position", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134751, + "status": "passed", + "title": "rejects when superior and subordinate are the same position" + }, + { + "ancestorTitles": [ + "PositionHierarchyService", + "link" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService link verifies both positions and links the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134755, + "status": "passed", + "title": "verifies both positions and links the relation" + }, + { + "ancestorTitles": [ + "PositionHierarchyService", + "unlink" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService unlink verifies both positions and unlinks the relation", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134756, + "status": "passed", + "title": "verifies both positions and unlinks the relation" + }, + { + "ancestorTitles": [ + "PositionHierarchyService", + "listSubordinates" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService listSubordinates resolves each subordinate position from the relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134757, + "status": "passed", + "title": "resolves each subordinate position from the relations" + }, + { + "ancestorTitles": [ + "PositionHierarchyService", + "listSuperiors" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyService listSuperiors resolves each superior position from the relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134759, + "status": "passed", + "title": "resolves each superior position from the relations" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134773, + "loadTestEnvironmentEnd": 1779141134713, + "loadTestEnvironmentStart": 1779141134711, + "runtime": 60, + "setupAfterEnvEnd": 1779141134765, + "setupAfterEnvStart": 1779141134740, + "setupFilesEnd": 1779141134713, + "setupFilesStart": 1779141134713, + "slow": false, + "start": 1779141134713 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/review.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewDomain create creates a review with all supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 19, + "retryReasons": [], + "startAt": 1779141134772, + "status": "passed", + "title": "creates a review with all supplied properties" + }, + { + "ancestorTitles": [ + "ReviewDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewDomain create defaults the stage to NEW when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134773, + "status": "passed", + "title": "defaults the stage to NEW when not provided" + }, + { + "ancestorTitles": [ + "ReviewDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewDomain create normalises missing optional fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134773, + "status": "passed", + "title": "normalises missing optional fields to null" + }, + { + "ancestorTitles": [ + "ReviewDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewDomain create preserves explicit null values for optional fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134773, + "status": "passed", + "title": "preserves explicit null values for optional fields" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134776, + "loadTestEnvironmentEnd": 1779141134729, + "loadTestEnvironmentStart": 1779141134724, + "runtime": 47, + "setupAfterEnvEnd": 1779141134767, + "setupAfterEnvStart": 1779141134739, + "setupFilesEnd": 1779141134729, + "setupFilesStart": 1779141134729, + "slow": false, + "start": 1779141134729 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/respondent.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RespondentMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentMapper toDomain converts a prisma row into a RespondentDomain instance", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134773, + "status": "passed", + "title": "converts a prisma row into a RespondentDomain instance" + }, + { + "ancestorTitles": [ + "RespondentMapper", + "toPrisma" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentMapper toPrisma serialises a RespondentDomain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134774, + "status": "passed", + "title": "serialises a RespondentDomain into a Prisma create input" + }, + { + "ancestorTitles": [ + "RespondentMapper", + "enum conversions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentMapper enum conversions uppercases the response status in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134775, + "status": "passed", + "title": "uppercases the response status in both directions" + }, + { + "ancestorTitles": [ + "RespondentMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RespondentMapper enum conversions uppercases the category in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134776, + "status": "passed", + "title": "uppercases the category in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134782, + "loadTestEnvironmentEnd": 1779141134698, + "loadTestEnvironmentStart": 1779141134695, + "runtime": 84, + "setupAfterEnvEnd": 1779141134767, + "setupAfterEnvStart": 1779141134728, + "setupFilesEnd": 1779141134698, + "setupFilesStart": 1779141134698, + "slow": false, + "start": 1779141134698 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review-stage-history.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageHistoryMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryMapper toDomain converts a prisma row into a ReviewStageHistoryDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134781, + "status": "passed", + "title": "converts a prisma row into a ReviewStageHistoryDomain" + }, + { + "ancestorTitles": [ + "ReviewStageHistoryMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryMapper toPrisma serialises a domain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134782, + "status": "passed", + "title": "serialises a domain into a Prisma create input" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 13, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134785, + "loadTestEnvironmentEnd": 1779141134734, + "loadTestEnvironmentStart": 1779141134731, + "runtime": 51, + "setupAfterEnvEnd": 1779141134773, + "setupAfterEnvStart": 1779141134748, + "setupFilesEnd": 1779141134734, + "setupFilesStart": 1779141134734, + "slow": false, + "start": 1779141134734 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/constants/review-stage-transitions.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows NEW to move to SELF_ASSESSMENT or CANCELED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134782, + "status": "passed", + "title": "allows NEW to move to SELF_ASSESSMENT or CANCELED only" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows SELF_ASSESSMENT to move to WAITING_TO_START, IN_PROGRESS, or CANCELED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134782, + "status": "passed", + "title": "allows SELF_ASSESSMENT to move to WAITING_TO_START, IN_PROGRESS, or CANCELED" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows WAITING_TO_START to move to IN_PROGRESS, FINISHED, or CANCELED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134783, + "status": "passed", + "title": "allows WAITING_TO_START to move to IN_PROGRESS, FINISHED, or CANCELED" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows IN_PROGRESS to move to FINISHED or CANCELED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134783, + "status": "passed", + "title": "allows IN_PROGRESS to move to FINISHED or CANCELED" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows FINISHED to move to PREPARING_REPORT only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134783, + "status": "passed", + "title": "allows FINISHED to move to PREPARING_REPORT only" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows PREPARING_REPORT to move into the report pipeline", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134783, + "status": "passed", + "title": "allows PREPARING_REPORT to move into the report pipeline" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows PROCESSING_BY_HR to move to PUBLISHED, ANALYSIS, or ARCHIVED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134783, + "status": "passed", + "title": "allows PROCESSING_BY_HR to move to PUBLISHED, ANALYSIS, or ARCHIVED" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows PUBLISHED to move to ANALYSIS or ARCHIVED", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134783, + "status": "passed", + "title": "allows PUBLISHED to move to ANALYSIS or ARCHIVED" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows ANALYSIS to move to ARCHIVED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134784, + "status": "passed", + "title": "allows ANALYSIS to move to ARCHIVED only" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS allows CANCELED to move to ARCHIVED only", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134784, + "status": "passed", + "title": "allows CANCELED to move to ARCHIVED only" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS treats ARCHIVED as a terminal stage", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134784, + "status": "passed", + "title": "treats ARCHIVED as a terminal stage" + }, + { + "ancestorTitles": [ + "REVIEW_STAGE_TRANSITIONS" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "REVIEW_STAGE_TRANSITIONS covers every ReviewStage as an entry key", + "invocations": 1, + "location": null, + "numPassingAsserts": 11, + "retryReasons": [], + "startAt": 1779141134784, + "status": "passed", + "title": "covers every ReviewStage as an entry key" + }, + { + "ancestorTitles": [ + "TRANSITION_REASONS (review)" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TRANSITION_REASONS (review) exposes the canonical reason strings", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134784, + "status": "passed", + "title": "exposes the canonical reason strings" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134797, + "loadTestEnvironmentEnd": 1779141134735, + "loadTestEnvironmentStart": 1779141134734, + "runtime": 62, + "setupAfterEnvEnd": 1779141134788, + "setupAfterEnvStart": 1779141134753, + "setupFilesEnd": 1779141134735, + "setupFilesStart": 1779141134735, + "slow": false, + "start": 1779141134735 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/domain/question-template.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionTemplateDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateDomain create creates a question template with every supplied property", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141134794, + "status": "passed", + "title": "creates a question template with every supplied property" + }, + { + "ancestorTitles": [ + "QuestionTemplateDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateDomain create defaults isForSelfassessment to false, status to ACTIVE, positionIds to []", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134795, + "status": "passed", + "title": "defaults isForSelfassessment to false, status to ACTIVE, positionIds to []" + }, + { + "ancestorTitles": [ + "QuestionTemplateDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateDomain create coerces null isForSelfassessment to false", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134796, + "status": "passed", + "title": "coerces null isForSelfassessment to false" + }, + { + "ancestorTitles": [ + "QuestionTemplateDomain", + "withPositions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionTemplateDomain withPositions returns a new instance carrying the supplied positionIds", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134796, + "status": "passed", + "title": "returns a new instance carrying the supplied positionIds" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134808, + "loadTestEnvironmentEnd": 1779141134718, + "loadTestEnvironmentStart": 1779141134717, + "runtime": 90, + "setupAfterEnvEnd": 1779141134774, + "setupAfterEnvStart": 1779141134739, + "setupFilesEnd": 1779141134718, + "setupFilesStart": 1779141134718, + "slow": false, + "start": 1779141134718 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/infrastructure/mail/nest-mailer.adapter.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "NestMailerAdapter", + "sendMail" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NestMailerAdapter sendMail forwards the options to the underlying mailer service", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134807, + "status": "passed", + "title": "forwards the options to the underlying mailer service" + }, + { + "ancestorTitles": [ + "NestMailerAdapter", + "sendMail" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NestMailerAdapter sendMail maps every attachment field through to nodemailer", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134808, + "status": "passed", + "title": "maps every attachment field through to nodemailer" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134817, + "loadTestEnvironmentEnd": 1779141134721, + "loadTestEnvironmentStart": 1779141134720, + "runtime": 96, + "setupAfterEnvEnd": 1779141134805, + "setupAfterEnvStart": 1779141134753, + "setupFilesEnd": 1779141134721, + "setupFilesStart": 1779141134721, + "slow": false, + "start": 1779141134721 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/domain/strategic-report-insight.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportInsightDomain", + "create" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightDomain create creates an insight with every decimal field coerced", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134814, + "status": "passed", + "title": "creates an insight with every decimal field coerced" + }, + { + "ancestorTitles": [ + "StrategicReportInsightDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightDomain create defaults missing decimal fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134816, + "status": "passed", + "title": "defaults missing decimal fields to null" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134827, + "loadTestEnvironmentEnd": 1779141134756, + "loadTestEnvironmentStart": 1779141134754, + "runtime": 70, + "setupAfterEnvEnd": 1779141134816, + "setupAfterEnvStart": 1779141134780, + "setupFilesEnd": 1779141134757, + "setupFilesStart": 1779141134757, + "slow": false, + "start": 1779141134757 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportMapper toDomain converts a prisma row into a StrategicReportDomain with empty relations", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134826, + "status": "passed", + "title": "converts a prisma row into a StrategicReportDomain with empty relations" + }, + { + "ancestorTitles": [ + "StrategicReportMapper", + "toDomainWithRelations" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportMapper toDomainWithRelations includes analytics and insights in the aggregate", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134826, + "status": "passed", + "title": "includes analytics and insights in the aggregate" + }, + { + "ancestorTitles": [ + "StrategicReportMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportMapper toPrisma serialises a domain object into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134827, + "status": "passed", + "title": "serialises a domain object into a Prisma create input" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 7, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134829, + "loadTestEnvironmentEnd": 1779141134779, + "loadTestEnvironmentStart": 1779141134778, + "runtime": 50, + "setupAfterEnvEnd": 1779141134818, + "setupAfterEnvStart": 1779141134793, + "setupFilesEnd": 1779141134779, + "setupFilesStart": 1779141134779, + "slow": false, + "start": 1779141134779 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/cycle.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleDomain create creates a cycle with the supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 14, + "retryReasons": [], + "startAt": 1779141134828, + "status": "passed", + "title": "creates a cycle with the supplied properties" + }, + { + "ancestorTitles": [ + "CycleDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleDomain create defaults the stage to NEW when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134829, + "status": "passed", + "title": "defaults the stage to NEW when not provided" + }, + { + "ancestorTitles": [ + "CycleDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleDomain create defaults isActive to true when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134829, + "status": "passed", + "title": "defaults isActive to true when not provided" + }, + { + "ancestorTitles": [ + "CycleDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleDomain create defaults minRespondentsThreshold to the anonymity threshold minimum", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134829, + "status": "passed", + "title": "defaults minRespondentsThreshold to the anonymity threshold minimum" + }, + { + "ancestorTitles": [ + "CycleDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleDomain create normalises missing description and deadlines to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134829, + "status": "passed", + "title": "normalises missing description and deadlines to null" + }, + { + "ancestorTitles": [ + "CycleDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleDomain create preserves explicit null values for optional fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134829, + "status": "passed", + "title": "preserves explicit null values for optional fields" + }, + { + "ancestorTitles": [ + "CycleDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleDomain create omits id, createdAt and updatedAt when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134829, + "status": "passed", + "title": "omits id, createdAt and updatedAt when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134835, + "loadTestEnvironmentEnd": 1779141134769, + "loadTestEnvironmentStart": 1779141134768, + "runtime": 66, + "setupAfterEnvEnd": 1779141134832, + "setupAfterEnvStart": 1779141134794, + "setupFilesEnd": 1779141134769, + "setupFilesStart": 1779141134769, + "slow": false, + "start": 1779141134769 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/domain/competence-question-template-relation.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationDomain create creates a relation with every supplied field", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134834, + "status": "passed", + "title": "creates a relation with every supplied field" + }, + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationDomain create omits id and createdAt when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134834, + "status": "passed", + "title": "omits id and createdAt when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134842, + "loadTestEnvironmentEnd": 1779141134777, + "loadTestEnvironmentStart": 1779141134775, + "runtime": 65, + "setupAfterEnvEnd": 1779141134838, + "setupAfterEnvStart": 1779141134795, + "setupFilesEnd": 1779141134777, + "setupFilesStart": 1779141134777, + "slow": false, + "start": 1779141134777 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/review-stage-history.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageHistoryDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryDomain create creates a history record with all supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134841, + "status": "passed", + "title": "creates a history record with all supplied properties" + }, + { + "ancestorTitles": [ + "ReviewStageHistoryDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageHistoryDomain create normalises missing actor/reason fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134842, + "status": "passed", + "title": "normalises missing actor/reason fields to null" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134845, + "loadTestEnvironmentEnd": 1779141134789, + "loadTestEnvironmentStart": 1779141134787, + "runtime": 56, + "setupAfterEnvEnd": 1779141134835, + "setupAfterEnvStart": 1779141134799, + "setupFilesEnd": 1779141134789, + "setupFilesStart": 1779141134789, + "slow": false, + "start": 1779141134789 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/events/cycle-stage-processed.event.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageProcessedEvent", + "fromCycleStageChangedEvent" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageProcessedEvent fromCycleStageChangedEvent maps the toStage to the processed-event currentStage", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134843, + "status": "passed", + "title": "maps the toStage to the processed-event currentStage" + }, + { + "ancestorTitles": [ + "CycleStageProcessedEvent", + "fromCycleStageChangedEvent" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageProcessedEvent fromCycleStageChangedEvent forwards optional actor/reason fields when undefined", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134844, + "status": "passed", + "title": "forwards optional actor/reason fields when undefined" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134852, + "loadTestEnvironmentEnd": 1779141134790, + "loadTestEnvironmentStart": 1779141134787, + "runtime": 62, + "setupAfterEnvEnd": 1779141134842, + "setupAfterEnvStart": 1779141134807, + "setupFilesEnd": 1779141134790, + "setupFilesStart": 1779141134790, + "slow": false, + "start": 1779141134790 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/domain/role.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RoleDomain", + "constructor" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleDomain constructor creates a role with every supplied property", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134849, + "status": "passed", + "title": "creates a role with every supplied property" + }, + { + "ancestorTitles": [ + "RoleDomain", + "constructor" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleDomain constructor normalises missing description to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134851, + "status": "passed", + "title": "normalises missing description to null" + }, + { + "ancestorTitles": [ + "RoleDomain", + "constructor" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleDomain constructor preserves explicit null description", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134852, + "status": "passed", + "title": "preserves explicit null description" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134855, + "loadTestEnvironmentEnd": 1779141134799, + "loadTestEnvironmentStart": 1779141134798, + "runtime": 56, + "setupAfterEnvEnd": 1779141134852, + "setupAfterEnvStart": 1779141134820, + "setupFilesEnd": 1779141134799, + "setupFilesStart": 1779141134799, + "slow": false, + "start": 1779141134799 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/infrastructure/mappers/position-question-template-relation.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationMapper toDomain converts a prisma row into a PositionQuestionTemplateRelationDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134854, + "status": "passed", + "title": "converts a prisma row into a PositionQuestionTemplateRelationDomain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134875, + "loadTestEnvironmentEnd": 1779141134811, + "loadTestEnvironmentStart": 1779141134809, + "runtime": 64, + "setupAfterEnvEnd": 1779141134864, + "setupAfterEnvStart": 1779141134836, + "setupFilesEnd": 1779141134811, + "setupFilesStart": 1779141134811, + "slow": false, + "start": 1779141134811 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/domain/strategic-report.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportDomain", + "create" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportDomain create creates a strategic report with every supplied field", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134871, + "status": "passed", + "title": "creates a strategic report with every supplied field" + }, + { + "ancestorTitles": [ + "StrategicReportDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportDomain create defaults missing decimal fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134873, + "status": "passed", + "title": "defaults missing decimal fields to null" + }, + { + "ancestorTitles": [ + "StrategicReportDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportDomain create defaults analytics and insights to empty arrays", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134874, + "status": "passed", + "title": "defaults analytics and insights to empty arrays" + }, + { + "ancestorTitles": [ + "StrategicReportDomain", + "withAnalytics / withInsights" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportDomain withAnalytics / withInsights returns a new instance with the supplied analytics", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134874, + "status": "passed", + "title": "returns a new instance with the supplied analytics" + }, + { + "ancestorTitles": [ + "StrategicReportDomain", + "withAnalytics / withInsights" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportDomain withAnalytics / withInsights returns a new instance with the supplied insights", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134874, + "status": "passed", + "title": "returns a new instance with the supplied insights" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134887, + "loadTestEnvironmentEnd": 1779141134840, + "loadTestEnvironmentStart": 1779141134837, + "runtime": 47, + "setupAfterEnvEnd": 1779141134876, + "setupAfterEnvStart": 1779141134856, + "setupFilesEnd": 1779141134840, + "setupFilesStart": 1779141134840, + "slow": false, + "start": 1779141134840 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/domain/notification-log.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "NotificationLogDomain", + "create" + ], + "duration": 5, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogDomain create creates a log with every supplied property", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134881, + "status": "passed", + "title": "creates a log with every supplied property" + }, + { + "ancestorTitles": [ + "NotificationLogDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogDomain create defaults channel to EMAIL when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134886, + "status": "passed", + "title": "defaults channel to EMAIL when not provided" + }, + { + "ancestorTitles": [ + "NotificationLogDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogDomain create normalises missing fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134886, + "status": "passed", + "title": "normalises missing fields to null" + }, + { + "ancestorTitles": [ + "NotificationLogDomain", + "isSent" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogDomain isSent returns false when sentAt is null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134887, + "status": "passed", + "title": "returns false when sentAt is null" + }, + { + "ancestorTitles": [ + "NotificationLogDomain", + "isSent" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogDomain isSent returns true when sentAt is a Date", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134887, + "status": "passed", + "title": "returns true when sentAt is a Date" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134888, + "loadTestEnvironmentEnd": 1779141134829, + "loadTestEnvironmentStart": 1779141134828, + "runtime": 59, + "setupAfterEnvEnd": 1779141134880, + "setupAfterEnvStart": 1779141134849, + "setupFilesEnd": 1779141134829, + "setupFilesStart": 1779141134829, + "slow": false, + "start": 1779141134829 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/cycle-stage-history.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CycleStageHistoryDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryDomain create creates a history record with all supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134887, + "status": "passed", + "title": "creates a history record with all supplied properties" + }, + { + "ancestorTitles": [ + "CycleStageHistoryDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CycleStageHistoryDomain create normalises missing actor/reason fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134888, + "status": "passed", + "title": "normalises missing actor/reason fields to null" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134897, + "loadTestEnvironmentEnd": 1779141134829, + "loadTestEnvironmentStart": 1779141134827, + "runtime": 68, + "setupAfterEnvEnd": 1779141134891, + "setupAfterEnvStart": 1779141134849, + "setupFilesEnd": 1779141134829, + "setupFilesStart": 1779141134829, + "slow": false, + "start": 1779141134829 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/domain/report-insight.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportInsightDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightDomain create creates a question insight with all decimal fields coerced", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134895, + "status": "passed", + "title": "creates a question insight with all decimal fields coerced" + }, + { + "ancestorTitles": [ + "ReportInsightDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightDomain create defaults missing decimal/optional fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134896, + "status": "passed", + "title": "defaults missing decimal/optional fields to null" + }, + { + "ancestorTitles": [ + "ReportInsightDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportInsightDomain create normalises competence fields to null for question insights", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134897, + "status": "passed", + "title": "normalises competence fields to null for question insights" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134902, + "loadTestEnvironmentEnd": 1779141134849, + "loadTestEnvironmentStart": 1779141134848, + "runtime": 53, + "setupAfterEnvEnd": 1779141134892, + "setupAfterEnvStart": 1779141134859, + "setupFilesEnd": 1779141134849, + "setupFilesStart": 1779141134849, + "slow": false, + "start": 1779141134849 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report-insight.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportInsightMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightMapper toDomain converts a prisma row into a StrategicReportInsightDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134899, + "status": "passed", + "title": "converts a prisma row into a StrategicReportInsightDomain" + }, + { + "ancestorTitles": [ + "StrategicReportInsightMapper", + "toPrisma" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightMapper toPrisma serialises with 4dp strings and undefined for null fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134900, + "status": "passed", + "title": "serialises with 4dp strings and undefined for null fields" + }, + { + "ancestorTitles": [ + "StrategicReportInsightMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightMapper enum conversions uppercases entity type in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134901, + "status": "passed", + "title": "uppercases entity type in both directions" + }, + { + "ancestorTitles": [ + "StrategicReportInsightMapper", + "enum conversions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportInsightMapper enum conversions uppercases insight type in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134901, + "status": "passed", + "title": "uppercases insight type in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134902, + "loadTestEnvironmentEnd": 1779141134846, + "loadTestEnvironmentStart": 1779141134845, + "runtime": 56, + "setupAfterEnvEnd": 1779141134897, + "setupAfterEnvStart": 1779141134862, + "setupFilesEnd": 1779141134846, + "setupFilesStart": 1779141134846, + "slow": false, + "start": 1779141134846 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/domain/team-membership.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamMembershipDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMembershipDomain create creates a membership with every supplied field", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134901, + "status": "passed", + "title": "creates a membership with every supplied field" + }, + { + "ancestorTitles": [ + "TeamMembershipDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMembershipDomain create defaults isPrimary to false when not supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134901, + "status": "passed", + "title": "defaults isPrimary to false when not supplied" + }, + { + "ancestorTitles": [ + "TeamMembershipDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMembershipDomain create coerces null isPrimary to false", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134901, + "status": "passed", + "title": "coerces null isPrimary to false" + }, + { + "ancestorTitles": [ + "TeamMembershipDomain", + "withUser" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMembershipDomain withUser returns a new instance carrying the supplied user", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134901, + "status": "passed", + "title": "returns a new instance carrying the supplied user" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 6, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134905, + "loadTestEnvironmentEnd": 1779141134839, + "loadTestEnvironmentStart": 1779141134837, + "runtime": 66, + "setupAfterEnvEnd": 1779141134894, + "setupAfterEnvStart": 1779141134859, + "setupFilesEnd": 1779141134839, + "setupFilesStart": 1779141134839, + "slow": false, + "start": 1779141134839 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/infrastructure/mappers/user.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "UserMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserMapper toDomain converts a prisma user into a UserDomain with empty roles when none are linked", + "invocations": 1, + "location": null, + "numPassingAsserts": 11, + "retryReasons": [], + "startAt": 1779141134901, + "status": "passed", + "title": "converts a prisma user into a UserDomain with empty roles when none are linked" + }, + { + "ancestorTitles": [ + "UserMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserMapper toDomain converts userRoles relations into IdentityRole values", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134902, + "status": "passed", + "title": "converts userRoles relations into IdentityRole values" + }, + { + "ancestorTitles": [ + "UserMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserMapper toDomain converts null secondName to undefined on the domain side", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134902, + "status": "passed", + "title": "converts null secondName to undefined on the domain side" + }, + { + "ancestorTitles": [ + "UserMapper", + "toPrisma" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserMapper toPrisma serialises a domain without roles into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134902, + "status": "passed", + "title": "serialises a domain without roles into a Prisma create input" + }, + { + "ancestorTitles": [ + "UserMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserMapper toPrisma emits a userRoles.createMany block when roles are present", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134904, + "status": "passed", + "title": "emits a userRoles.createMany block when roles are present" + }, + { + "ancestorTitles": [ + "UserMapper", + "status conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserMapper status conversions uppercases identity status in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134905, + "status": "passed", + "title": "uppercases identity status in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134930, + "loadTestEnvironmentEnd": 1779141134859, + "loadTestEnvironmentStart": 1779141134858, + "runtime": 71, + "setupAfterEnvEnd": 1779141134927, + "setupAfterEnvStart": 1779141134875, + "setupFilesEnd": 1779141134859, + "setupFilesStart": 1779141134859, + "slow": false, + "start": 1779141134859 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/domain/position.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionDomain create creates a position with every supplied property", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134928, + "status": "passed", + "title": "creates a position with every supplied property" + }, + { + "ancestorTitles": [ + "PositionDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionDomain create normalises missing description to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134929, + "status": "passed", + "title": "normalises missing description to null" + }, + { + "ancestorTitles": [ + "PositionDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionDomain create preserves explicit null description", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134929, + "status": "passed", + "title": "preserves explicit null description" + }, + { + "ancestorTitles": [ + "PositionDomain", + "withPatch" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionDomain withPatch returns a new instance carrying the supplied patch", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134929, + "status": "passed", + "title": "returns a new instance carrying the supplied patch" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134935, + "loadTestEnvironmentEnd": 1779141134855, + "loadTestEnvironmentStart": 1779141134854, + "runtime": 80, + "setupAfterEnvEnd": 1779141134931, + "setupAfterEnvStart": 1779141134879, + "setupFilesEnd": 1779141134855, + "setupFilesStart": 1779141134855, + "slow": false, + "start": 1779141134855 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/infrastructure/mappers/role.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "RoleMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleMapper toDomain converts a prisma role into a RoleDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141134935, + "status": "passed", + "title": "converts a prisma role into a RoleDomain" + }, + { + "ancestorTitles": [ + "RoleMapper", + "toPrismaRole / fromPrismaRole" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "RoleMapper toPrismaRole / fromPrismaRole uppercases the role code in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134935, + "status": "passed", + "title": "uppercases the role code in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134939, + "loadTestEnvironmentEnd": 1779141134888, + "loadTestEnvironmentStart": 1779141134886, + "runtime": 51, + "setupAfterEnvEnd": 1779141134935, + "setupAfterEnvStart": 1779141134899, + "setupFilesEnd": 1779141134888, + "setupFilesStart": 1779141134888, + "slow": false, + "start": 1779141134888 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/cluster-score.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreDomain create creates a cluster score with all supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 10, + "retryReasons": [], + "startAt": 1779141134937, + "status": "passed", + "title": "creates a cluster score with all supplied properties" + }, + { + "ancestorTitles": [ + "ClusterScoreDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreDomain create coerces a numeric score input into a Decimal instance", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134938, + "status": "passed", + "title": "coerces a numeric score input into a Decimal instance" + }, + { + "ancestorTitles": [ + "ClusterScoreDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreDomain create accepts a string score input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134938, + "status": "passed", + "title": "accepts a string score input" + }, + { + "ancestorTitles": [ + "ClusterScoreDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreDomain create defaults the cycleId to null when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134938, + "status": "passed", + "title": "defaults the cycleId to null when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134951, + "loadTestEnvironmentEnd": 1779141134891, + "loadTestEnvironmentStart": 1779141134890, + "runtime": 60, + "setupAfterEnvEnd": 1779141134948, + "setupAfterEnvStart": 1779141134925, + "setupFilesEnd": 1779141134891, + "setupFilesStart": 1779141134891, + "slow": false, + "start": 1779141134891 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/infrastructure/mappers/team-membership.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamMembershipMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMembershipMapper toDomain converts a prisma row into a TeamMembershipDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134950, + "status": "passed", + "title": "converts a prisma row into a TeamMembershipDomain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134967, + "loadTestEnvironmentEnd": 1779141134922, + "loadTestEnvironmentStart": 1779141134921, + "runtime": 44, + "setupAfterEnvEnd": 1779141134965, + "setupAfterEnvStart": 1779141134931, + "setupFilesEnd": 1779141134923, + "setupFilesStart": 1779141134923, + "slow": false, + "start": 1779141134923 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/domain/competence.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceDomain create creates a competence with every supplied property", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134966, + "status": "passed", + "title": "creates a competence with every supplied property" + }, + { + "ancestorTitles": [ + "CompetenceDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceDomain create normalises missing code and description to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134967, + "status": "passed", + "title": "normalises missing code and description to null" + }, + { + "ancestorTitles": [ + "CompetenceDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceDomain create preserves explicit null code and description", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134967, + "status": "passed", + "title": "preserves explicit null code and description" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 7, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134968, + "loadTestEnvironmentEnd": 1779141134911, + "loadTestEnvironmentStart": 1779141134910, + "runtime": 57, + "setupAfterEnvEnd": 1779141134959, + "setupAfterEnvStart": 1779141134927, + "setupFilesEnd": 1779141134911, + "setupFilesStart": 1779141134911, + "slow": false, + "start": 1779141134911 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/domain/user.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "UserDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserDomain create creates a user with every supplied property", + "invocations": 1, + "location": null, + "numPassingAsserts": 14, + "retryReasons": [], + "startAt": 1779141134966, + "status": "passed", + "title": "creates a user with every supplied property" + }, + { + "ancestorTitles": [ + "UserDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserDomain create builds fullName from first/last when secondName is missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134967, + "status": "passed", + "title": "builds fullName from first/last when secondName is missing" + }, + { + "ancestorTitles": [ + "UserDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserDomain create builds fullName including secondName when supplied", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134967, + "status": "passed", + "title": "builds fullName including secondName when supplied" + }, + { + "ancestorTitles": [ + "UserDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserDomain create defaults status to ACTIVE, avatarUrl/positionId/teamId/managerId to null, roles to []", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134967, + "status": "passed", + "title": "defaults status to ACTIVE, avatarUrl/positionId/teamId/managerId to null, roles to []" + }, + { + "ancestorTitles": [ + "UserDomain", + "withRoles" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserDomain withRoles returns a new instance with the supplied roles", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134967, + "status": "passed", + "title": "returns a new instance with the supplied roles" + }, + { + "ancestorTitles": [ + "UserDomain", + "withPatch" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserDomain withPatch returns a new instance with the supplied patch applied", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134967, + "status": "passed", + "title": "returns a new instance with the supplied patch applied" + }, + { + "ancestorTitles": [ + "UserDomain", + "withPatch" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserDomain withPatch rebuilds fullName when name parts are patched (via default fallback)", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141134968, + "status": "passed", + "title": "rebuilds fullName when name parts are patched (via default fallback)" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134970, + "loadTestEnvironmentEnd": 1779141134891, + "loadTestEnvironmentStart": 1779141134889, + "runtime": 79, + "setupAfterEnvEnd": 1779141134967, + "setupAfterEnvStart": 1779141134899, + "setupFilesEnd": 1779141134891, + "setupFilesStart": 1779141134891, + "slow": false, + "start": 1779141134891 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/domain/position-question-template-relation.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationDomain create creates a relation with every supplied field", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141134969, + "status": "passed", + "title": "creates a relation with every supplied field" + }, + { + "ancestorTitles": [ + "PositionQuestionTemplateRelationDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionQuestionTemplateRelationDomain create omits id and createdAt when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134969, + "status": "passed", + "title": "omits id and createdAt when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134974, + "loadTestEnvironmentEnd": 1779141134901, + "loadTestEnvironmentStart": 1779141134900, + "runtime": 72, + "setupAfterEnvEnd": 1779141134966, + "setupAfterEnvStart": 1779141134930, + "setupFilesEnd": 1779141134902, + "setupFilesStart": 1779141134902, + "slow": false, + "start": 1779141134902 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review-question-relation.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewQuestionRelationMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationMapper toDomain converts a prisma row into a ReviewQuestionRelationDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 11, + "retryReasons": [], + "startAt": 1779141134973, + "status": "passed", + "title": "converts a prisma row into a ReviewQuestionRelationDomain" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationMapper toDomain normalises null competence code/description and isForSelfassessment", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141134974, + "status": "passed", + "title": "normalises null competence code/description and isForSelfassessment" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationMapper toPrisma serialises a domain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134974, + "status": "passed", + "title": "serialises a domain into a Prisma create input" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134990, + "loadTestEnvironmentEnd": 1779141134942, + "loadTestEnvironmentStart": 1779141134941, + "runtime": 48, + "setupAfterEnvEnd": 1779141134986, + "setupAfterEnvStart": 1779141134959, + "setupFilesEnd": 1779141134942, + "setupFilesStart": 1779141134942, + "slow": false, + "start": 1779141134942 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/infrastructure/mappers/notification-log.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "NotificationLogMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogMapper toDomain converts a prisma row into a NotificationLogDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141134988, + "status": "passed", + "title": "converts a prisma row into a NotificationLogDomain" + }, + { + "ancestorTitles": [ + "NotificationLogMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogMapper toPrisma serialises a domain object into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134989, + "status": "passed", + "title": "serialises a domain object into a Prisma create input" + }, + { + "ancestorTitles": [ + "NotificationLogMapper", + "enum conversions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogMapper enum conversions passes the notification kind through unchanged in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134989, + "status": "passed", + "title": "passes the notification kind through unchanged in both directions" + }, + { + "ancestorTitles": [ + "NotificationLogMapper", + "enum conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationLogMapper enum conversions passes the notification channel through unchanged in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141134990, + "status": "passed", + "title": "passes the notification channel through unchanged in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141134993, + "loadTestEnvironmentEnd": 1779141134938, + "loadTestEnvironmentStart": 1779141134937, + "runtime": 55, + "setupAfterEnvEnd": 1779141134991, + "setupAfterEnvStart": 1779141134947, + "setupFilesEnd": 1779141134938, + "setupFilesStart": 1779141134938, + "slow": false, + "start": 1779141134938 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/domain/notification-kind.enum.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "NotificationKind" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationKind exposes the expected notification kinds with string values matching the keys", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141134992, + "status": "passed", + "title": "exposes the expected notification kinds with string values matching the keys" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135001, + "loadTestEnvironmentEnd": 1779141134921, + "loadTestEnvironmentStart": 1779141134920, + "runtime": 80, + "setupAfterEnvEnd": 1779141134998, + "setupAfterEnvStart": 1779141134941, + "setupFilesEnd": 1779141134921, + "setupFilesStart": 1779141134921, + "slow": false, + "start": 1779141134921 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/domain/cluster.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterDomain create creates a cluster with every supplied property", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141134999, + "status": "passed", + "title": "creates a cluster with every supplied property" + }, + { + "ancestorTitles": [ + "ClusterDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterDomain create leaves id, createdAt, updatedAt undefined when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141135000, + "status": "passed", + "title": "leaves id, createdAt, updatedAt undefined when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135004, + "loadTestEnvironmentEnd": 1779141134970, + "loadTestEnvironmentStart": 1779141134968, + "runtime": 34, + "setupAfterEnvEnd": 1779141135002, + "setupAfterEnvStart": 1779141134983, + "setupFilesEnd": 1779141134970, + "setupFilesStart": 1779141134970, + "slow": false, + "start": 1779141134970 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/infrastructure/mappers/position.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionMapper toDomain converts a prisma row into a PositionDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141135003, + "status": "passed", + "title": "converts a prisma row into a PositionDomain" + }, + { + "ancestorTitles": [ + "PositionMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionMapper toDomain preserves null description from prisma", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135004, + "status": "passed", + "title": "preserves null description from prisma" + }, + { + "ancestorTitles": [ + "PositionMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionMapper toPrisma serialises a domain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135004, + "status": "passed", + "title": "serialises a domain into a Prisma create input" + }, + { + "ancestorTitles": [ + "PositionMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionMapper toPrisma passes through null description when present on the domain", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135004, + "status": "passed", + "title": "passes through null description when present on the domain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135013, + "loadTestEnvironmentEnd": 1779141134963, + "loadTestEnvironmentStart": 1779141134962, + "runtime": 50, + "setupAfterEnvEnd": 1779141135011, + "setupAfterEnvStart": 1779141134977, + "setupFilesEnd": 1779141134963, + "setupFilesStart": 1779141134963, + "slow": false, + "start": 1779141134963 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/infrastructure/templates/logo-data-uri.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "logo-data-uri" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "logo-data-uri exposes the canonical CID and filename", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141135012, + "status": "passed", + "title": "exposes the canonical CID and filename" + }, + { + "ancestorTitles": [ + "logo-data-uri" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "logo-data-uri encodes a PNG data URI matching the buffer", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135012, + "status": "passed", + "title": "encodes a PNG data URI matching the buffer" + }, + { + "ancestorTitles": [ + "logo-data-uri" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "logo-data-uri starts the decoded buffer with the PNG magic header", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135013, + "status": "passed", + "title": "starts the decoded buffer with the PNG magic header" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135023, + "loadTestEnvironmentEnd": 1779141134934, + "loadTestEnvironmentStart": 1779141134933, + "runtime": 89, + "setupAfterEnvEnd": 1779141135021, + "setupAfterEnvStart": 1779141134962, + "setupFilesEnd": 1779141134934, + "setupFilesStart": 1779141134934, + "slow": false, + "start": 1779141134934 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/domain/position-competence-relation.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionCompetenceRelationDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationDomain create creates a relation with every supplied field", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141135023, + "status": "passed", + "title": "creates a relation with every supplied field" + }, + { + "ancestorTitles": [ + "PositionCompetenceRelationDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationDomain create omits id and createdAt when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135023, + "status": "passed", + "title": "omits id and createdAt when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135039, + "loadTestEnvironmentEnd": 1779141134980, + "loadTestEnvironmentStart": 1779141134979, + "runtime": 59, + "setupAfterEnvEnd": 1779141135036, + "setupAfterEnvStart": 1779141134992, + "setupFilesEnd": 1779141134980, + "setupFilesStart": 1779141134980, + "slow": false, + "start": 1779141134980 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/infrastructure/mappers/position-hierarchy.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionHierarchyMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyMapper toDomain converts a prisma row into a PositionHierarchyDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135038, + "status": "passed", + "title": "converts a prisma row into a PositionHierarchyDomain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135049, + "loadTestEnvironmentEnd": 1779141134993, + "loadTestEnvironmentStart": 1779141134992, + "runtime": 56, + "setupAfterEnvEnd": 1779141135042, + "setupAfterEnvStart": 1779141135006, + "setupFilesEnd": 1779141134993, + "setupFilesStart": 1779141134993, + "slow": false, + "start": 1779141134993 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/cluster-score-analytics.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterScoreAnalyticsDomain", + "create" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsDomain create creates analytics with Decimal-coerced numeric fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 19, + "retryReasons": [], + "startAt": 1779141135047, + "status": "passed", + "title": "creates analytics with Decimal-coerced numeric fields" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsDomain create accepts string numeric inputs and Decimal instances", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135049, + "status": "passed", + "title": "accepts string numeric inputs and Decimal instances" + }, + { + "ancestorTitles": [ + "ClusterScoreAnalyticsDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterScoreAnalyticsDomain create omits id, createdAt and updatedAt when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141135049, + "status": "passed", + "title": "omits id, createdAt and updatedAt when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135071, + "loadTestEnvironmentEnd": 1779141135001, + "loadTestEnvironmentStart": 1779141134999, + "runtime": 70, + "setupAfterEnvEnd": 1779141135068, + "setupAfterEnvStart": 1779141135014, + "setupFilesEnd": 1779141135001, + "setupFilesStart": 1779141135001, + "slow": false, + "start": 1779141135001 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/infrastructure/mappers/competence.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceMapper toDomain converts a prisma row into a CompetenceDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135070, + "status": "passed", + "title": "converts a prisma row into a CompetenceDomain" + }, + { + "ancestorTitles": [ + "CompetenceMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceMapper toDomain preserves null code/description coming from the database", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135071, + "status": "passed", + "title": "preserves null code/description coming from the database" + }, + { + "ancestorTitles": [ + "CompetenceMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceMapper toPrisma serialises a domain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135071, + "status": "passed", + "title": "serialises a domain into a Prisma create input" + }, + { + "ancestorTitles": [ + "CompetenceMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceMapper toPrisma passes through null code/description when present on the domain", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135071, + "status": "passed", + "title": "passes through null code/description when present on the domain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135074, + "loadTestEnvironmentEnd": 1779141134973, + "loadTestEnvironmentStart": 1779141134971, + "runtime": 101, + "setupAfterEnvEnd": 1779141135072, + "setupAfterEnvStart": 1779141135021, + "setupFilesEnd": 1779141134973, + "setupFilesStart": 1779141134973, + "slow": false, + "start": 1779141134973 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/reviewer.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewerDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerDomain create creates a reviewer with all supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141135073, + "status": "passed", + "title": "creates a reviewer with all supplied properties" + }, + { + "ancestorTitles": [ + "ReviewerDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerDomain create normalises missing team fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135073, + "status": "passed", + "title": "normalises missing team fields to null" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135087, + "loadTestEnvironmentEnd": 1779141135007, + "loadTestEnvironmentStart": 1779141135005, + "runtime": 80, + "setupAfterEnvEnd": 1779141135083, + "setupAfterEnvStart": 1779141135027, + "setupFilesEnd": 1779141135007, + "setupFilesStart": 1779141135007, + "slow": false, + "start": 1779141135007 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/review-question-relation.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewQuestionRelationDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationDomain create creates a relation with all supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 11, + "retryReasons": [], + "startAt": 1779141135087, + "status": "passed", + "title": "creates a relation with all supplied properties" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationDomain create normalises missing competence code/description to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135087, + "status": "passed", + "title": "normalises missing competence code/description to null" + }, + { + "ancestorTitles": [ + "ReviewQuestionRelationDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewQuestionRelationDomain create defaults isForSelfassessment to false when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135087, + "status": "passed", + "title": "defaults isForSelfassessment to false when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135091, + "loadTestEnvironmentEnd": 1779141135015, + "loadTestEnvironmentStart": 1779141135014, + "runtime": 76, + "setupAfterEnvEnd": 1779141135088, + "setupAfterEnvStart": 1779141135054, + "setupFilesEnd": 1779141135015, + "setupFilesStart": 1779141135015, + "slow": false, + "start": 1779141135015 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/reviewer.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewerMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerMapper toDomain converts a prisma row into a ReviewerDomain instance", + "invocations": 1, + "location": null, + "numPassingAsserts": 10, + "retryReasons": [], + "startAt": 1779141135090, + "status": "passed", + "title": "converts a prisma row into a ReviewerDomain instance" + }, + { + "ancestorTitles": [ + "ReviewerMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewerMapper toPrisma serialises a ReviewerDomain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135090, + "status": "passed", + "title": "serialises a ReviewerDomain into a Prisma create input" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135091, + "loadTestEnvironmentEnd": 1779141135037, + "loadTestEnvironmentStart": 1779141135035, + "runtime": 54, + "setupAfterEnvEnd": 1779141135088, + "setupAfterEnvStart": 1779141135049, + "setupFilesEnd": 1779141135037, + "setupFilesStart": 1779141135037, + "slow": false, + "start": 1779141135037 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/infrastructure/mappers/competence-question-template-relation.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "CompetenceQuestionTemplateRelationMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "CompetenceQuestionTemplateRelationMapper toDomain converts a prisma row into a CompetenceQuestionTemplateRelationDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135090, + "status": "passed", + "title": "converts a prisma row into a CompetenceQuestionTemplateRelationDomain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 7, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135095, + "loadTestEnvironmentEnd": 1779141134974, + "loadTestEnvironmentStart": 1779141134971, + "runtime": 121, + "setupAfterEnvEnd": 1779141135076, + "setupAfterEnvStart": 1779141134993, + "setupFilesEnd": 1779141134974, + "setupFilesStart": 1779141134974, + "slow": false, + "start": 1779141134974 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/domain/report.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportDomain create creates a report with every supplied numeric field as Decimal", + "invocations": 1, + "location": null, + "numPassingAsserts": 13, + "retryReasons": [], + "startAt": 1779141135093, + "status": "passed", + "title": "creates a report with every supplied numeric field as Decimal" + }, + { + "ancestorTitles": [ + "ReportDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportDomain create defaults cycleId and missing decimal fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135094, + "status": "passed", + "title": "defaults cycleId and missing decimal fields to null" + }, + { + "ancestorTitles": [ + "ReportDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportDomain create defaults analytics, comments, and insights to empty arrays", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141135094, + "status": "passed", + "title": "defaults analytics, comments, and insights to empty arrays" + }, + { + "ancestorTitles": [ + "ReportDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportDomain create preserves explicit null on optional decimal fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135094, + "status": "passed", + "title": "preserves explicit null on optional decimal fields" + }, + { + "ancestorTitles": [ + "ReportDomain", + "withAnalytics / withComments / withInsights" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportDomain withAnalytics / withComments / withInsights returns a new instance carrying the supplied analytics", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141135094, + "status": "passed", + "title": "returns a new instance carrying the supplied analytics" + }, + { + "ancestorTitles": [ + "ReportDomain", + "withAnalytics / withComments / withInsights" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportDomain withAnalytics / withComments / withInsights returns a new instance carrying the supplied comments", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135095, + "status": "passed", + "title": "returns a new instance carrying the supplied comments" + }, + { + "ancestorTitles": [ + "ReportDomain", + "withAnalytics / withComments / withInsights" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportDomain withAnalytics / withComments / withInsights returns a new instance carrying the supplied insights", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135095, + "status": "passed", + "title": "returns a new instance carrying the supplied insights" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135103, + "loadTestEnvironmentEnd": 1779141135004, + "loadTestEnvironmentStart": 1779141135003, + "runtime": 99, + "setupAfterEnvEnd": 1779141135094, + "setupAfterEnvStart": 1779141135022, + "setupFilesEnd": 1779141135004, + "setupFilesStart": 1779141135004, + "slow": false, + "start": 1779141135004 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/answer.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "AnswerMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerMapper toDomain converts a prisma row into an AnswerDomain instance", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141135102, + "status": "passed", + "title": "converts a prisma row into an AnswerDomain instance" + }, + { + "ancestorTitles": [ + "AnswerMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerMapper toPrisma serialises an AnswerDomain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141135103, + "status": "passed", + "title": "serialises an AnswerDomain into a Prisma create input" + }, + { + "ancestorTitles": [ + "AnswerMapper", + "category conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerMapper category conversions uppercases the category in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135103, + "status": "passed", + "title": "uppercases the category in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135117, + "loadTestEnvironmentEnd": 1779141135073, + "loadTestEnvironmentStart": 1779141135072, + "runtime": 44, + "setupAfterEnvEnd": 1779141135112, + "setupAfterEnvStart": 1779141135087, + "setupFilesEnd": 1779141135073, + "setupFilesStart": 1779141135073, + "slow": false, + "start": 1779141135073 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/infrastructure/mappers/organisation.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "OrganisationMapper", + "toTeamDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "OrganisationMapper toTeamDomain converts a prisma team row into a TeamDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141135116, + "status": "passed", + "title": "converts a prisma team row into a TeamDomain" + }, + { + "ancestorTitles": [ + "OrganisationMapper", + "toTeamMembershipDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "OrganisationMapper toTeamMembershipDomain converts a prisma membership row into a TeamMembershipDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141135117, + "status": "passed", + "title": "converts a prisma membership row into a TeamMembershipDomain" + }, + { + "ancestorTitles": [ + "OrganisationMapper", + "toPositionDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "OrganisationMapper toPositionDomain converts a prisma position row into a PositionDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141135117, + "status": "passed", + "title": "converts a prisma position row into a PositionDomain" + }, + { + "ancestorTitles": [ + "OrganisationMapper", + "toPositionHierarchyDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "OrganisationMapper toPositionHierarchyDomain converts a prisma hierarchy relation into a PositionHierarchyDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141135117, + "status": "passed", + "title": "converts a prisma hierarchy relation into a PositionHierarchyDomain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135134, + "loadTestEnvironmentEnd": 1779141135056, + "loadTestEnvironmentStart": 1779141135055, + "runtime": 78, + "setupAfterEnvEnd": 1779141135131, + "setupAfterEnvStart": 1779141135079, + "setupFilesEnd": 1779141135056, + "setupFilesStart": 1779141135056, + "slow": false, + "start": 1779141135056 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/infrastructure/mappers/position-competence-relation.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionCompetenceRelationMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionCompetenceRelationMapper toDomain converts a prisma row into a PositionCompetenceRelationDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135133, + "status": "passed", + "title": "converts a prisma row into a PositionCompetenceRelationDomain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135136, + "loadTestEnvironmentEnd": 1779141135093, + "loadTestEnvironmentStart": 1779141135092, + "runtime": 43, + "setupAfterEnvEnd": 1779141135127, + "setupAfterEnvStart": 1779141135109, + "setupFilesEnd": 1779141135093, + "setupFilesStart": 1779141135093, + "slow": false, + "start": 1779141135093 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/question.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionDomain create creates a question with all supplied properties", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141135133, + "status": "passed", + "title": "creates a question with all supplied properties" + }, + { + "ancestorTitles": [ + "QuestionDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionDomain create normalises missing optional foreign keys to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141135134, + "status": "passed", + "title": "normalises missing optional foreign keys to null" + }, + { + "ancestorTitles": [ + "QuestionDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionDomain create defaults isForSelfassessment to false when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135134, + "status": "passed", + "title": "defaults isForSelfassessment to false when not provided" + }, + { + "ancestorTitles": [ + "QuestionDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionDomain create coerces null isForSelfassessment to false", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135135, + "status": "passed", + "title": "coerces null isForSelfassessment to false" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135153, + "loadTestEnvironmentEnd": 1779141135059, + "loadTestEnvironmentStart": 1779141135057, + "runtime": 94, + "setupAfterEnvEnd": 1779141135143, + "setupAfterEnvStart": 1779141135094, + "setupFilesEnd": 1779141135059, + "setupFilesStart": 1779141135059, + "slow": false, + "start": 1779141135059 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/domain/report-comment.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportCommentDomain", + "create" + ], + "duration": 2, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentDomain create creates a comment with all supplied fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141135150, + "status": "passed", + "title": "creates a comment with all supplied fields" + }, + { + "ancestorTitles": [ + "ReportCommentDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentDomain create defaults respondentCategories to an empty array when null is passed", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135152, + "status": "passed", + "title": "defaults respondentCategories to an empty array when null is passed" + }, + { + "ancestorTitles": [ + "ReportCommentDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportCommentDomain create defaults sentiment to null when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135152, + "status": "passed", + "title": "defaults sentiment to null when missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135163, + "loadTestEnvironmentEnd": 1779141135106, + "loadTestEnvironmentStart": 1779141135105, + "runtime": 57, + "setupAfterEnvEnd": 1779141135148, + "setupAfterEnvStart": 1779141135114, + "setupFilesEnd": 1779141135106, + "setupFilesStart": 1779141135106, + "slow": false, + "start": 1779141135106 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/review.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewMapper toDomain converts a prisma row into a ReviewDomain instance", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141135160, + "status": "passed", + "title": "converts a prisma row into a ReviewDomain instance" + }, + { + "ancestorTitles": [ + "ReviewMapper", + "toPrisma" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewMapper toPrisma serialises a ReviewDomain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141135161, + "status": "passed", + "title": "serialises a ReviewDomain into a Prisma create input" + }, + { + "ancestorTitles": [ + "ReviewMapper", + "stage conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewMapper stage conversions uppercases the stage when going to Prisma", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135162, + "status": "passed", + "title": "uppercases the stage when going to Prisma" + }, + { + "ancestorTitles": [ + "ReviewMapper", + "stage conversions" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewMapper stage conversions uppercases the stage when coming from Prisma", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135162, + "status": "passed", + "title": "uppercases the stage when coming from Prisma" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135172, + "loadTestEnvironmentEnd": 1779141135105, + "loadTestEnvironmentStart": 1779141135092, + "runtime": 67, + "setupAfterEnvEnd": 1779141135169, + "setupAfterEnvStart": 1779141135119, + "setupFilesEnd": 1779141135105, + "setupFilesStart": 1779141135105, + "slow": false, + "start": 1779141135105 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/identity/application/events/user-created.event.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "UserCreatedEvent" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "UserCreatedEvent exposes userId and email as readonly fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135171, + "status": "passed", + "title": "exposes userId and email as readonly fields" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135165, + "loadTestEnvironmentEnd": 1779141135104, + "loadTestEnvironmentStart": 1779141135103, + "runtime": 61, + "setupAfterEnvEnd": 1779141135149, + "setupAfterEnvStart": 1779141135115, + "setupFilesEnd": 1779141135104, + "setupFilesStart": 1779141135104, + "slow": false, + "start": 1779141135104 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/infrastructure/mappers/strategic-report-analytics.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "StrategicReportAnalyticsMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsMapper toDomain converts a prisma row into a StrategicReportAnalyticsDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141135153, + "status": "passed", + "title": "converts a prisma row into a StrategicReportAnalyticsDomain" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsMapper", + "toPrisma" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsMapper toPrisma serialises with 4dp strings and undefined for null fields", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141135154, + "status": "passed", + "title": "serialises with 4dp strings and undefined for null fields" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsMapper", + "toDecimalString" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsMapper toDecimalString returns undefined for null/undefined", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135155, + "status": "passed", + "title": "returns undefined for null/undefined" + }, + { + "ancestorTitles": [ + "StrategicReportAnalyticsMapper", + "toDecimalString" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "StrategicReportAnalyticsMapper toDecimalString rounds to 4 decimal places", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135155, + "status": "passed", + "title": "rounds to 4 decimal places" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135176, + "loadTestEnvironmentEnd": 1779141135120, + "loadTestEnvironmentStart": 1779141135119, + "runtime": 56, + "setupAfterEnvEnd": 1779141135169, + "setupAfterEnvStart": 1779141135132, + "setupFilesEnd": 1779141135120, + "setupFilesStart": 1779141135120, + "slow": false, + "start": 1779141135120 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/application/events/review-stage-processed.event.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReviewStageProcessedEvent", + "fromReviewStageChangedEvent" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageProcessedEvent fromReviewStageChangedEvent maps the toStage to the processed-event currentStage", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141135176, + "status": "passed", + "title": "maps the toStage to the processed-event currentStage" + }, + { + "ancestorTitles": [ + "ReviewStageProcessedEvent", + "fromReviewStageChangedEvent" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReviewStageProcessedEvent fromReviewStageChangedEvent forwards optional actor/reason fields when undefined", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135176, + "status": "passed", + "title": "forwards optional actor/reason fields when undefined" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 3, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135178, + "loadTestEnvironmentEnd": 1779141135092, + "loadTestEnvironmentStart": 1779141135089, + "runtime": 86, + "setupAfterEnvEnd": 1779141135173, + "setupAfterEnvStart": 1779141135119, + "setupFilesEnd": 1779141135092, + "setupFilesStart": 1779141135092, + "slow": false, + "start": 1779141135092 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/domain/report-analytics.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportAnalyticsDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsDomain create creates a question analytics row with every decimal coerced", + "invocations": 1, + "location": null, + "numPassingAsserts": 7, + "retryReasons": [], + "startAt": 1779141135176, + "status": "passed", + "title": "creates a question analytics row with every decimal coerced" + }, + { + "ancestorTitles": [ + "ReportAnalyticsDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsDomain create creates a competence analytics row, normalising missing question fields to null", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135177, + "status": "passed", + "title": "creates a competence analytics row, normalising missing question fields to null" + }, + { + "ancestorTitles": [ + "ReportAnalyticsDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsDomain create defaults all decimal fields to null when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141135177, + "status": "passed", + "title": "defaults all decimal fields to null when missing" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135178, + "loadTestEnvironmentEnd": 1779141135085, + "loadTestEnvironmentStart": 1779141135082, + "runtime": 93, + "setupAfterEnvEnd": 1779141135169, + "setupAfterEnvStart": 1779141135109, + "setupFilesEnd": 1779141135085, + "setupFilesStart": 1779141135085, + "slow": false, + "start": 1779141135085 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/infrastructure/mappers/question.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "QuestionMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionMapper toDomain converts a prisma row into a QuestionDomain instance", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141135175, + "status": "passed", + "title": "converts a prisma row into a QuestionDomain instance" + }, + { + "ancestorTitles": [ + "QuestionMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionMapper toDomain falls back isForSelfassessment to false when null", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135176, + "status": "passed", + "title": "falls back isForSelfassessment to false when null" + }, + { + "ancestorTitles": [ + "QuestionMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionMapper toPrisma serialises a QuestionDomain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135177, + "status": "passed", + "title": "serialises a QuestionDomain into a Prisma create input" + }, + { + "ancestorTitles": [ + "QuestionMapper", + "answer type conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "QuestionMapper answer type conversions uppercases the answer type in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135177, + "status": "passed", + "title": "uppercases the answer type in both directions" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135205, + "loadTestEnvironmentEnd": 1779141135139, + "loadTestEnvironmentStart": 1779141135136, + "runtime": 66, + "setupAfterEnvEnd": 1779141135199, + "setupAfterEnvStart": 1779141135169, + "setupFilesEnd": 1779141135139, + "setupFilesStart": 1779141135139, + "slow": false, + "start": 1779141135139 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/library/infrastructure/mappers/cluster.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ClusterMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterMapper toDomain converts a prisma row into a ClusterDomain, coercing decimals to numbers", + "invocations": 1, + "location": null, + "numPassingAsserts": 9, + "retryReasons": [], + "startAt": 1779141135204, + "status": "passed", + "title": "converts a prisma row into a ClusterDomain, coercing decimals to numbers" + }, + { + "ancestorTitles": [ + "ClusterMapper", + "toPrisma" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ClusterMapper toPrisma serialises a domain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135204, + "status": "passed", + "title": "serialises a domain into a Prisma create input" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 1, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135219, + "loadTestEnvironmentEnd": 1779141135166, + "loadTestEnvironmentStart": 1779141135164, + "runtime": 53, + "setupAfterEnvEnd": 1779141135217, + "setupAfterEnvStart": 1779141135190, + "setupFilesEnd": 1779141135166, + "setupFilesStart": 1779141135166, + "slow": false, + "start": 1779141135166 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/notifications/domain/notification-channel.enum.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "NotificationChannel" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "NotificationChannel exposes the EMAIL channel value", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135218, + "status": "passed", + "title": "exposes the EMAIL channel value" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 2, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135219, + "loadTestEnvironmentEnd": 1779141135144, + "loadTestEnvironmentStart": 1779141135142, + "runtime": 75, + "setupAfterEnvEnd": 1779141135217, + "setupAfterEnvStart": 1779141135193, + "setupFilesEnd": 1779141135144, + "setupFilesStart": 1779141135144, + "slow": false, + "start": 1779141135144 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/domain/position-hierarchy.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "PositionHierarchyDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyDomain create creates a hierarchy relation with every supplied field", + "invocations": 1, + "location": null, + "numPassingAsserts": 4, + "retryReasons": [], + "startAt": 1779141135219, + "status": "passed", + "title": "creates a hierarchy relation with every supplied field" + }, + { + "ancestorTitles": [ + "PositionHierarchyDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "PositionHierarchyDomain create omits id and createdAt when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135219, + "status": "passed", + "title": "omits id and createdAt when not provided" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135227, + "loadTestEnvironmentEnd": 1779141135180, + "loadTestEnvironmentStart": 1779141135178, + "runtime": 47, + "setupAfterEnvEnd": 1779141135224, + "setupAfterEnvStart": 1779141135194, + "setupFilesEnd": 1779141135180, + "setupFilesStart": 1779141135180, + "slow": false, + "start": 1779141135180 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/organisation/infrastructure/mappers/team.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "TeamMapper", + "toDomain" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMapper toDomain converts a prisma row into a TeamDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 5, + "retryReasons": [], + "startAt": 1779141135226, + "status": "passed", + "title": "converts a prisma row into a TeamDomain" + }, + { + "ancestorTitles": [ + "TeamMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMapper toDomain preserves null description and headId from prisma", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135227, + "status": "passed", + "title": "preserves null description and headId from prisma" + }, + { + "ancestorTitles": [ + "TeamMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMapper toPrisma serialises a domain into a Prisma create input", + "invocations": 1, + "location": null, + "numPassingAsserts": 1, + "retryReasons": [], + "startAt": 1779141135227, + "status": "passed", + "title": "serialises a domain into a Prisma create input" + }, + { + "ancestorTitles": [ + "TeamMapper", + "toPrisma" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "TeamMapper toPrisma passes through null description and headId when present on the domain", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135227, + "status": "passed", + "title": "passes through null description and headId when present on the domain" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 5, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135231, + "loadTestEnvironmentEnd": 1779141135169, + "loadTestEnvironmentStart": 1779141135167, + "runtime": 62, + "setupAfterEnvEnd": 1779141135225, + "setupAfterEnvStart": 1779141135194, + "setupFilesEnd": 1779141135169, + "setupFilesStart": 1779141135169, + "slow": false, + "start": 1779141135169 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/reporting/infrastructure/mappers/report-analytics.mapper.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "ReportAnalyticsMapper", + "toDomain" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsMapper toDomain converts a prisma row into a ReportAnalyticsDomain", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141135230, + "status": "passed", + "title": "converts a prisma row into a ReportAnalyticsDomain" + }, + { + "ancestorTitles": [ + "ReportAnalyticsMapper", + "toPrisma" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsMapper toPrisma serialises a domain object into a Prisma create input with 4dp strings", + "invocations": 1, + "location": null, + "numPassingAsserts": 6, + "retryReasons": [], + "startAt": 1779141135230, + "status": "passed", + "title": "serialises a domain object into a Prisma create input with 4dp strings" + }, + { + "ancestorTitles": [ + "ReportAnalyticsMapper", + "entity-type conversions" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsMapper entity-type conversions uppercases the entity type in both directions", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135231, + "status": "passed", + "title": "uppercases the entity type in both directions" + }, + { + "ancestorTitles": [ + "ReportAnalyticsMapper", + "toDecimalString" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsMapper toDecimalString returns undefined for null/undefined inputs", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135231, + "status": "passed", + "title": "returns undefined for null/undefined inputs" + }, + { + "ancestorTitles": [ + "ReportAnalyticsMapper", + "toDecimalString" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "ReportAnalyticsMapper toDecimalString rounds to exactly 4 decimal places", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135231, + "status": "passed", + "title": "rounds to exactly 4 decimal places" + } + ], + "failureMessage": null + }, + { + "leaks": false, + "numFailingTests": 0, + "numPassingTests": 4, + "numPendingTests": 0, + "numTodoTests": 0, + "openHandles": [], + "perfStats": { + "end": 1779141135231, + "loadTestEnvironmentEnd": 1779141135176, + "loadTestEnvironmentStart": 1779141135175, + "runtime": 55, + "setupAfterEnvEnd": 1779141135227, + "setupAfterEnvStart": 1779141135192, + "setupFilesEnd": 1779141135176, + "setupFilesStart": 1779141135176, + "slow": false, + "start": 1779141135176 + }, + "skipped": false, + "snapshot": { + "added": 0, + "fileDeleted": false, + "matched": 0, + "unchecked": 0, + "uncheckedKeys": [], + "unmatched": 0, + "updated": 0 + }, + "testFilePath": "/Users/inessarepeshko/university/semestr7/diploma/intra/intra/apps/api/test/unit/contexts/feedback360/domain/answer.domain.spec.ts", + "testResults": [ + { + "ancestorTitles": [ + "AnswerDomain", + "create" + ], + "duration": 1, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerDomain create creates a numerical answer with all properties set", + "invocations": 1, + "location": null, + "numPassingAsserts": 8, + "retryReasons": [], + "startAt": 1779141135230, + "status": "passed", + "title": "creates a numerical answer with all properties set" + }, + { + "ancestorTitles": [ + "AnswerDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerDomain create creates a text answer with all properties set", + "invocations": 1, + "location": null, + "numPassingAsserts": 3, + "retryReasons": [], + "startAt": 1779141135231, + "status": "passed", + "title": "creates a text answer with all properties set" + }, + { + "ancestorTitles": [ + "AnswerDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerDomain create defaults numericalValue and textValue to null when missing", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135231, + "status": "passed", + "title": "defaults numericalValue and textValue to null when missing" + }, + { + "ancestorTitles": [ + "AnswerDomain", + "create" + ], + "duration": 0, + "failing": false, + "failureDetails": [], + "failureMessages": [], + "fullName": "AnswerDomain create omits id and createdAt when not provided", + "invocations": 1, + "location": null, + "numPassingAsserts": 2, + "retryReasons": [], + "startAt": 1779141135231, + "status": "passed", + "title": "omits id and createdAt when not provided" + } + ], + "failureMessage": null + } + ], + "wasInterrupted": false +} \ No newline at end of file diff --git a/apps/api/test/unit/setup-jest.ts b/apps/api/test/unit/setup-jest.ts new file mode 100644 index 00000000..81d57aae --- /dev/null +++ b/apps/api/test/unit/setup-jest.ts @@ -0,0 +1,22 @@ +/** + * Jest setup file for the unit suite. + * + * Silences NestJS Logger output globally so specs that hit + * swallow-error / log-on-failure branches don't drown the Jest + * reporter. Per-spec `jest.spyOn(Logger.prototype, ...)` still works + * — those spies override the global mock for the duration of the + * spec — so existing tests that assert on the Logger are unaffected. + * + * Errors that genuinely fail a test still surface through Jest's own + * reporter (expect rejections, thrown assertions, etc.), so silencing + * the Logger here does not hide real failures. + */ +import { Logger } from '@nestjs/common'; + +const noop = () => undefined; + +jest.spyOn(Logger.prototype, 'error').mockImplementation(noop); +jest.spyOn(Logger.prototype, 'warn').mockImplementation(noop); +jest.spyOn(Logger.prototype, 'log').mockImplementation(noop); +jest.spyOn(Logger.prototype, 'debug').mockImplementation(noop); +jest.spyOn(Logger.prototype, 'verbose').mockImplementation(noop); diff --git a/apps/api/test/users/users.controller.spec.ts b/apps/api/test/users/users.controller.spec.ts deleted file mode 100644 index b642eed0..00000000 --- a/apps/api/test/users/users.controller.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UsersService } from '../../src/modules/identity/application/users.service'; -import { UsersRepository } from '../../src/modules/identity/infrastructure/prisma-repositories/users.repository'; -import { UsersController } from '../../src/modules/identity/presentation/http/controllers/users.controller'; - -describe('UsersController', () => { - let controller: UsersController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [UsersController], - providers: [ - UsersService, - { - provide: UsersRepository, - useValue: { - create: jest.fn(), - findAll: jest.fn(), - findById: jest.fn(), - findByEmail: jest.fn(), - updateById: jest.fn(), - deleteById: jest.fn(), - }, - }, - ], - }).compile(); - - controller = module.get(UsersController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/api/test/users/users.service.spec.ts b/apps/api/test/users/users.service.spec.ts deleted file mode 100644 index b95a8c52..00000000 --- a/apps/api/test/users/users.service.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UsersService } from '../../src/modules/identity/application/users.service'; -import { UsersRepository } from '../../src/modules/identity/infrastructure/prisma-repositories/users.repository'; - -describe('UsersService', () => { - let service: UsersService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - UsersService, - { - provide: UsersRepository, - useValue: { - create: jest.fn(), - findAll: jest.fn(), - findById: jest.fn(), - findByEmail: jest.fn(), - updateById: jest.fn(), - deleteById: jest.fn(), - }, - }, - ], - }).compile(); - - service = module.get(UsersService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/api/test/web-e2e/.gitignore b/apps/api/test/web-e2e/.gitignore new file mode 100644 index 00000000..2155924c --- /dev/null +++ b/apps/api/test/web-e2e/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +cypress/videos/ +cypress/screenshots/ +cypress/downloads/ +cypress/reports/ +metrics.json +metrics.md +metrics.csv +*.log diff --git a/apps/api/test/web-e2e/README.md b/apps/api/test/web-e2e/README.md new file mode 100644 index 00000000..66d63f3f --- /dev/null +++ b/apps/api/test/web-e2e/README.md @@ -0,0 +1,202 @@ +# @intra/web-e2e — Cypress usability tests + +End-to-end usability tests for the Intra 360° Feedback web application, +designed to feed the **usability metrics table** in the thesis. + +## Rationale — why these scenarios + +In line with ISO 9241-11, three measurable usability metrics are captured: + +| Metric | Source in this suite | +| --------------- | --------------------------------------------------- | +| Effectiveness | Cypress assertion outcomes (success = task done). | +| Efficiency | `stepCount` and `durationMs` per scenario in `metrics.json`. | +| Error tolerance | Negative paths (invalid email, forbidden routes). | + +The six scenarios were selected on a **persona × task** matrix — one +important task per role plus a few cross-role checks — instead of one test per +page. Page-level checks belong to component / integration tests, not +usability research. + +| # | Scenario | Persona | Why it's here | +| - | -------------------------------------------------- | ---------- | ------------------------------------------------ | +| 1 | Sign-in (redirect, invalid email, happy path) | Guest | Entry point — clarity of feedback on bad input. | +| 2 | HR important path: team → position → competence → question template → cycle | HR | Longest flow; dominates the efficiency table. | +| 3 | Self-assessment fill | Employee | Form completion friction. | +| 4 | Anonymous peer review | Manager | Anonymity messaging must be visible BEFORE typing.| +| 5 | Individual report + comment | Employee | Readability of analytical visualisations. | +| 6 | Strategic report + cluster-score analytics filter | HR | Reaction of charts to filter changes. | + +## Prerequisites + +1. The test PostgreSQL container is up (`pnpm docker:up` from the repo root). +2. The API runs against `.env.test`: `pnpm start:test` (port `8080`). +3. The web app runs against `.env.test`: `cd apps/web && pnpm env:test -- next dev` (port `3000`). +4. Test DB is seeded: `pnpm db:test:refresh` from the repo root. + +> If you forget step 4, the `apiLogin` command fails on the very first +> spec — none of the seeded users will exist. + +## Running + +From the repository root: + +```bash +# One-time, before each run — refresh DB and execute every spec headlessly: +pnpm test:e2e:refresh + +# Interactive (Cypress runner UI): +pnpm test:e2e:open + +# Headless without DB refresh (faster, but assumes clean seed): +pnpm test:e2e + +# Print the metrics table after a run: +pnpm test:e2e:report +``` + +Sample output of `pnpm test:e2e:report`: + +``` +Scenario | Steps | Duration (s) | Success +---------------------------------------+--------+---------------+-------- +auth.redirect-unauthenticated | 2 | 0.42 | yes +auth.invalid-email | 4 | 1.61 | yes +auth.ui-login.hr | 2 | 1.28 | yes +hr.important-path.create-cycle | 17 | 24.85 | yes +employee.self-assessment | 6 | 5.93 | yes +peer.anonymous-review | 6 | 4.31 | yes +employee.individual-report | 6 | 3.74 | yes +hr.strategic-report | 3 | 2.18 | yes +hr.cluster-analytics.filter | 3 | 2.47 | yes + +Scenarios : 9 +Success : 100.0% +Avg steps : 5.4 +Avg time : 5.20 s +``` + +The raw rows live in `apps/api/test/web-e2e/metrics.json` (gitignored) — copy into +the thesis as a CSV / LaTeX table. + +## Configuration + +`cypress.config.ts` reads from `.env.test`: + +- `APP_FRONTEND_PROTOCOL`/`HOST`/`PORT` → `baseUrl` (default `http://localhost:3000`). +- `NEXT_PUBLIC_API_URL` → API URL for `cy.apiLogin()` (default `http://localhost:8080`). + +Seeded test users (in `Cypress.env('users')`): + +- `admin` — `oleksandr.bondarenko@intra.com` +- `hr` — `natalya.tkachenko@intra.com` +- `manager` — `pavlo.lytvyn@intra.com` +- `employee` — `yulia.kravchenko@intra.com` + +## Custom commands + +| Command | Purpose | +| ---------------------------------------- | --------------------------------------------- | +| `cy.apiLogin(email)` | Programmatic dev login — bypasses signin UI. | +| `cy.uiLogin(email)` | Real UI signin — used only by the auth spec. | +| `cy.fieldByLabel(text)` | Get an input by its `