# Goals Module Objectives (OKR-style goals): goal cycles, goals, progress tracking, weights, formula chains, owners, and per-instance configuration. Hexagonal architecture (business / infrastructure / presentation) with TypeDI and Sequelize. ## Directory Structure ``` goals/ ├── business/ │ ├── constants/ # Module constants │ ├── interfaces/ # Input/output contracts (create/update/duplicate, etc.) │ ├── models/ # 11 domain models (Goal, GoalCycle, GoalProgress, ...) │ ├── ports/ # 11 abstract ports (dependency inversion) │ ├── services/ # 9 service classes (see Service Map) │ └── types/ # Enums/value types (status, valueType, visibility, ...) ├── infrastructure/ │ ├── adapters/ # Concrete port implementations │ ├── dataAccessObjects/ # Sequelize DAOs │ └── mappers/ # Pure mapper functions (DAO ↔ Domain) └── presentation/ ├── controllers/ # goalsController, goalCyclesController, goalFormulaChainsController, publicApiGoalController ├── routers/ # Router registration ├── dataTransferObjects/ # Response DTOs ├── serializationClasses/ # Serialization classes └── validationClasses/ # Validation classes (VCs) ``` ## Service Map — Which Service Owns Each Feature | Feature Area | Service | |---|---| | Goal CRUD, progress updates, listing, owners-in-cycle stats | `GoalsService` | | Goal cycle lifecycle (create/update/start/finish/delete/list, duplication) | `GoalCyclesService` | | Goal progress entries (history) | `GoalProgressService` | | Goal weights within a cycle | `GoalWeightsService` | | Goal owners | `GoalOwnersService` | | Per-instance goal config (goal flow, formulas, boss-can-edit) | `goalCycleConfigurationsService` | | Formula chains (computed goals) | `goalFormulaChainsService` | | SQS queue consumer for cycles | `goalCyclesQueueConsumer` | | CDC service | `goalsCDCService` | `GoalsService` and `GoalCyclesService` reference each other; the cross-dependency is broken with lazy property injection (`@Inject(() => ...)` setters), not constructor injection. ## Key Domain Concepts ### Configuration scoping (IMPORTANT) There are **two** config surfaces, and they are scoped differently: - **Per-instance** — `GoalCycleConfiguration` (table `GoalCycleConfigurations`, PK = `instanceId`). Despite the name, this is **instance-wide**, not per-cycle. Holds `goalFlowEnabled`, `mathFormulasEnabled`, `bossCanEditPublishedObjectives`. Managed by `goalCycleConfigurationsService` / `goalCyclesController.upsertCyclesConfiguration`. - **Per-cycle** — fields on the `GoalCycle` model itself (table `GoalCycles`): `enforceGoalCountBounds`, `minGoalsPerOwner`, `maxGoalsPerOwner`, `maxProgressPercent`, `hasFormulas`, `isForIndividuals`. Set via the create/update cycle flow. When adding a new **cycle-level** setting, mirror the `minGoalsPerOwner` / `maxGoalsPerOwner` / `maxProgressPercent` pattern across: `GoalCycle` model (constructor + `updateBeforeStarting`), `GoalCycleDAO` (+ `GoalCycleCreationFields`), `goalCycleMapper` (all 3 functions, incl. `goalCycleToDuplicateInterface`), `GoalCycleDTO`, `GoalCycleSC`, `CreateGoalCycleInterface` (inherited by Update/Duplicate), and `createGoalCycleVC`. A migration adds the column to `GoalCycles`. Per-cycle settings are only writable **before the cycle starts**: `GoalCycle.updateBeforeStarting()` (DRAFT/PENDING) sets them; `GoalCycle.updateWhenInProgress()` (IN_PROGRESS/FINISHED) ignores everything except `name`/`startDate`/`endDate`. The existing value round-trips unchanged through `saveCycle`. ### Progress pipeline (`Goal` model) Progress mutation always runs this order (in `changeProgress` → `handleDeltaValue`/`handleTotalValue`): 1. `calculateProgress()` — computes raw `progress` percentage from `currentValue`, `startingValue`, `objectiveValue`. **Pure**: no clamping. 2. `ensureCompletionCoherence()` — snaps to/from 100% for completion coherence. 3. `clampProgressToMax(maxProgressPercent)` — **final** step. Caps the visible `progress` at the cap; `null` means no cap. Must run last (after `ensureCompletionCoherence`, which can raise progress back to 100). `currentValue` (the accumulated raw value) is **never** clamped — only the visible `progress` is. This preserves progress history while limiting the displayed percentage. ### Progress cap (`maxProgressPercent`) The cap is a **per-cycle** setting (1–100, or `null` for no cap). It is resolved per goal by `GoalsService.getMaxProgressPercentForGoal(goal)`: - Goal **with** a cycle → uses that cycle's `maxProgressPercent` (loaded via `GoalCyclesService.findCycleByPk`). - Goal **without** a cycle (`goalCycleId === null`) → **no cap** (`null`). It is applied in `GoalsService.createProgress` and `GoalsService.update` (the latter calls `calculateProgress()` then `clampProgressToMax(...)` as an explicit final step). > Performance reviews snapshot a goal's score when the review is filled/completed. Changing a goal's progress or a cycle's `maxProgressPercent` afterwards does **not** retroactively change a completed review's score. This is existing snapshot behaviour, not specific to the cap. ### Duplication Instance duplication (`src/api/services/duplication.ts`) copies cycles via `GoalCyclesService.createCyclesOnly`, building each from `goalCycleToDuplicateInterface(sourceCycle)`. `DuplicateGoalCycleInterface extends CreateGoalCycleInterface`, so any field added to `CreateGoalCycleInterface` + the mapper is automatically duplicated. Source cycles are read through `goalCycleDAOToGoalCycle`, so any new DAO column must be added to that mapper to survive duplication. ## Testing - Unit tests: `humand-packages/monolith/test/modules/goals/`. Shared fixtures in `test/modules/goals/common.ts` (`generateRandomGoal`, `generateRandomGoalCycle`, `generateRandomGoalCycleConfiguration`, ...). Run: `pnpm nx run monolith:test-all -- --testPathPattern="test/modules/goals"`. - In `goalService.test.ts`, the `GoalCyclesService` mock's `findCycleByPk` has a default `mockResolvedValue(generateRandomGoalCycle())` in the top-level `beforeEach`, because `createProgress`/`update` resolve the cap through it. Override it when asserting cap behaviour. ## Keeping This Document Up to Date After every change to this module, check whether this file needs updating: - **New service / service method** → update the Service Map (and add detail if it introduces a new concept). - **New cycle-level vs instance-level setting** → confirm it landed on the right surface (`GoalCycle` vs `GoalCycleConfiguration`) and update the Configuration scoping section. - **Change to the progress pipeline or cap resolution** → update Key Domain Concepts. - **Port/adapter/model added or removed** → update the Directory Structure counts. The document is wrong if it describes code that no longer exists, or omits code that does. Keep it exact.