Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-03
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## Context

`LEISTUNGSNACHWEIS` (proof-of-performance) month-end tasks are generated in `MonthEndTaskPlanningService.planProjectTasks(...)`. Today the sole gate is `project.billable() && !activeLeadIds.isEmpty()`. Project leads cannot suppress the task for projects where the workflow does not apply.

The `Project` aggregate lives in the `project` bounded context and is largely ZEP-synced (`name`, dates, `billable`), with `leads` being the one MEGA-managed field. Month-end generation reads projects through the anti-corruption boundary `MonthEndProjectSnapshot`, produced by `ProjectSnapshotAdapter`/`MonthEndProjectSnapshotMapper`. The `project` BC currently exposes **no inbound adapter** — projects only leave the BC as internal snapshots for month-end.

There is no existing frontend path to read a lead's projects, so the toggle needs both a read and a write endpoint to be usable.

## Goals / Non-Goals

**Goals:**
- Let a project lead opt a specific project out of (and back into) `LEISTUNGSNACHWEIS` generation.
- Preserve current behaviour for every existing project (default enabled).
- Keep the flag owned by the `project` BC and carried to month-end via the existing snapshot boundary.
- Provide the read + write REST surface the frontend needs.

**Non-Goals:**
- Making other task types (`ABRECHNUNG`, `PROJECT_LEAD_REVIEW`, `EMPLOYEE_TIME_CHECK`) configurable.
- Retroactively adding or removing already-generated tasks when the flag changes.
- Any generic "task-type configuration" framework.
- Sourcing the flag from ZEP.

## Decisions

### The flag lives on the `Project` aggregate, defaulting to `true`
`leistungsnachweisEnabled` is a MEGA-managed boolean on `Project`, alongside `leads`. It defaults to `true` so opt-out semantics preserve today's behaviour. `Project.create(...)` sets it `true`; `withSyncedZepData(...)` and `withLeads(...)` preserve it; a new `withLeistungsnachweisEnabled(boolean)` returns a new instance with the flag changed.

*Alternative considered:* a monthend-owned configuration keyed by project id. Rejected — `billable` already lives on `Project` and drives the same task, so co-locating keeps the gate's inputs in one aggregate and avoids a second source of truth.

### Generation reads the flag at generation time (no retroactivity)
`MonthEndProjectSnapshot` gains `leistungsnachweisEnabled`; `MonthEndTaskPlanningService` changes the gate to `project.billable() && !activeLeadIds.isEmpty() && project.leistungsnachweisEnabled()`. Because the value is evaluated only during a generation run, toggling affects the next run only. Idempotent regeneration is unchanged: turning the flag off does not delete existing tasks, and turning it on adds the task on the next run.

*Alternative considered:* reconcile open `LEISTUNGSNACHWEIS` tasks immediately on toggle. Rejected as out of scope and more complex; batch-at-generation matches the existing generation model.

### First inbound adapter on the `project` BC; two-layer authorization
A new `ProjectResource` implements the generated `ProjectApi`:
- `GET /projects` → `GetLeadProjectsUseCase`, returns the caller's led projects (via the existing `ProjectRepository.findAllByLead`). The `PROJECT_LEAD` role gate alone is sufficient because results are already scoped to the caller.
- `PUT /projects/{projectId}/leistungsnachweis-enabled` → `SetLeistungsnachweisEnabledUseCase`, which loads the project, asserts `project.leads().contains(actorId)` (throwing an authorization error otherwise), applies `withLeistungsnachweisEnabled(enabled)`, and saves. The `PROJECT_LEAD` role gate is not enough here — a lead of project A must not toggle project B — so the per-project lead check is enforced in the application service.

This follows the established pattern (`@MegaRolesAllowed`, `AuthenticatedActorContext.userId()`) and the codebase convention that cross-project authorization is a domain/application concern, not just a role annotation.

### Persistence
`ProjectEntity` gains a `leistungsnachweis_enabled` boolean column (`nullable = false`). A Liquibase changelog adds it with `defaultValueBoolean: true` so existing rows backfill to enabled. The entity↔domain MapStruct mapper carries the field both ways.

### DTO shape
`GET /projects` returns items of `{ id, zepId, name, billable, leistungsnachweisEnabled }`. `billable` is included so the UI can convey that the toggle has no effect on non-billable projects. `PUT` takes `{ enabled: boolean }`.

## Risks / Trade-offs

- **Default `false` would silently disable every billable project's proof-of-performance at the next month-end.** → Column defaults to `true` (opt-out) and the Liquibase change backfills existing rows to `true`.
- **A lead toggling another project's flag.** → Per-project `leads().contains(actorId)` check in the application service, not just the role gate.
- **Toggling mid-month is invisible until the next generation run** (could confuse users expecting immediate effect). → Accepted and documented as intended non-retroactive behaviour; the frontend can message this.
- **Introducing the first inbound adapter on the `project` BC** adds REST/OpenAPI surface where there was none. → Kept minimal (two endpoints) and consistent with existing resource conventions.

## Migration Plan

1. Add the Liquibase changelog (`leistungsnachweis_enabled` column, default `true`, not null) — backfills existing projects to enabled.
2. Deploy backend (domain gate, snapshot, endpoints). Behaviour is unchanged until a lead actively disables a project.
3. Frontend ships the configuration page consuming the new endpoints.

Rollback: the flag defaults to enabled, so reverting the code leaves generation behaving exactly as before; the extra column is inert if unused.

## Open Questions

None outstanding — default (opt-out), non-retroactivity, single-boolean scope, aggregate ownership, and the read+write surface were all settled during exploration.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Why

Today a `LEISTUNGSNACHWEIS` month-end task is generated for every billable project that has active leads — project leads have no way to suppress it for projects where the proof-of-performance workflow does not apply. Project leads need a per-project switch to opt out of `LEISTUNGSNACHWEIS` generation.

## What Changes

- The `Project` aggregate gains a `leistungsnachweisEnabled` boolean, **defaulting to `true`** (opt-out semantics — existing behaviour is preserved for all current projects).
- ZEP resync preserves the flag (it is MEGA-managed, like project leads); it is never overwritten from ZEP.
- `MonthEndProjectSnapshot` carries the flag into the month-end context.
- Month-end task generation gates `LEISTUNGSNACHWEIS` on the flag: a task is created only when the project is billable, has active leads, **and** `leistungsnachweisEnabled` is `true`. The flag is read at generation time, so a toggle change only affects the next generation run (no retroactive add/removal of already-generated tasks).
- A new inbound REST surface on the `project` bounded context (its first inbound adapter):
- `GET /projects` — returns the authenticated project lead's own projects with their `leistungsnachweisEnabled` state, so the frontend can render an editable toggle page.
- `PUT /projects/{projectId}/leistungsnachweis-enabled` — sets the flag. Authorized by the `PROJECT_LEAD` role **and** a per-project check that the caller is a lead of that specific project.

## Capabilities

### New Capabilities
- `project-rest-api`: inbound REST endpoints on the project bounded context for a project lead to read their led projects and toggle per-project `LEISTUNGSNACHWEIS` generation, with role-based and per-project lead authorization.

### Modified Capabilities
- `project-aggregate`: the `Project` aggregate and its `MonthEndProjectSnapshot` read model gain a `leistungsnachweisEnabled` flag (default `true`), preserved across ZEP resync and lead sync.
- `monthend-task-generation`: `LEISTUNGSNACHWEIS` generation is additionally gated on the project's `leistungsnachweisEnabled` flag.

## Impact

- `Project` (project domain aggregate) and `ZepProjectProfile` interaction (flag preserved, not sourced from ZEP)
- `MonthEndProjectSnapshot` (month-end read model) + its mapper
- `MonthEndTaskPlanningService` (generation gating logic)
- `ProjectEntity` + new Liquibase changelog (new `leistungsnachweis_enabled` column, default `true`)
- New `ProjectResource` inbound adapter, `GetLeadProjectsUseCase` / `SetLeistungsnachweisEnabledUseCase` and application services in the project BC
- OpenAPI schema (new project read DTO + toggle request DTO, generated `ProjectApi`)
- Frontend: new project-lead configuration page consuming the two endpoints
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## MODIFIED Requirements

### Requirement: Generation creates project-owned month-end tasks with eligible leads
The system SHALL generate project-owned month-end tasks for active projects using the leads assigned at generation time. It MUST create one `PROJECT_LEAD_REVIEW` task per active assigned employee, one `LEISTUNGSNACHWEIS` task per active assigned employee on a billable project **whose `leistungsnachweisEnabled` flag is true**, and one `ABRECHNUNG` task per billable project, each with the eligible project leads captured at generation time.

The `leistungsnachweisEnabled` flag SHALL be read from the project snapshot at generation time. A change to the flag SHALL only affect subsequent generation runs; it SHALL NOT retroactively add or remove already-generated `LEISTUNGSNACHWEIS` tasks. The flag SHALL gate `LEISTUNGSNACHWEIS` only; `PROJECT_LEAD_REVIEW` and `ABRECHNUNG` generation SHALL be unaffected by it.

#### Scenario: Lead review is generated once per employee
- **WHEN** an active project has active assigned employees and at least one active lead
- **THEN** the system creates one `PROJECT_LEAD_REVIEW` task per active assigned employee with all active project leads as eligible actors

#### Scenario: Leistungsnachweis is generated once per employee on an enabled billable project
- **WHEN** an active billable project has active assigned employees, at least one active lead, and `leistungsnachweisEnabled` is true
- **THEN** the system creates one `LEISTUNGSNACHWEIS` task per active assigned employee with all active project leads as eligible actors

#### Scenario: Leistungsnachweis is not generated when the project flag is disabled
- **WHEN** an active billable project has active assigned employees and at least one active lead but `leistungsnachweisEnabled` is false
- **THEN** the system does not create `LEISTUNGSNACHWEIS` tasks for that project
- **THEN** `PROJECT_LEAD_REVIEW` and `ABRECHNUNG` tasks are still generated for that project

#### Scenario: Leistungsnachweis is not generated when billable project has no active leads
- **WHEN** an active billable project has active assigned employees but no active leads
- **THEN** the system does not create `LEISTUNGSNACHWEIS` tasks for that project

#### Scenario: Non-billable project does not generate Leistungsnachweis
- **WHEN** an active employee is assigned to a non-billable active project during month-end generation
- **THEN** the system does not create a `LEISTUNGSNACHWEIS` task for that assignment

#### Scenario: Disabling the flag does not remove existing tasks
- **WHEN** a `LEISTUNGSNACHWEIS` task already exists for a project month and the project's `leistungsnachweisEnabled` flag is set to false afterwards
- **THEN** the existing `LEISTUNGSNACHWEIS` task is not removed
- **THEN** the next generation run for that month does not create additional `LEISTUNGSNACHWEIS` tasks for that project

#### Scenario: Abrechnung is generated once per billable project
- **WHEN** an active billable project has at least one active lead
- **THEN** the system creates one `ABRECHNUNG` task for the project with all active project leads as eligible actors
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## MODIFIED Requirements

### Requirement: Project aggregate encapsulates identity and master data
The `Project` aggregate SHALL be modeled as an immutable, record-oriented type holding a stable internal `ProjectId` (UUID), a ZEP numeric id (`zepId`), a unique `name`, a `startDate`, an optional `endDate`, a `billable` boolean flag, a `leistungsnachweisEnabled` boolean flag, and a set of `UserId` references representing project leads. State transitions such as ZEP resync and project lead sync SHALL return new Project instances instead of mutating existing state. The aggregate SHALL NOT hold any workflow state (that is the concern of a future capability).

The `leistungsnachweisEnabled` flag is MEGA-managed (not sourced from ZEP) and SHALL default to `true` on project creation. It SHALL be preserved across ZEP resync and project lead sync. A dedicated state transition SHALL return a new Project instance with the flag set to a caller-provided value.

`MonthEndProjectSnapshot` — the monthend-specific read model derived from `Project` — SHALL carry `{ ProjectId id, int zepId, String name, boolean billable, boolean leistungsnachweisEnabled, Set<UserId> leadIds }` only. It SHALL NOT carry `startDate` or `endDate`; activeness filtering for a given month SHALL be enforced in the adapter before returning the snapshot to the application layer.

#### Scenario: Project created from ZEP profile data
- **WHEN** `Project.create(ProjectId, ZepProjectProfile)` is called
- **THEN** a new immutable Project instance is returned with name, zepId, startDate, endDate, and billable populated from the profile
- **THEN** the leads set is empty
- **THEN** `leistungsnachweisEnabled` is `true`

#### Scenario: Project reconstituted from persisted state
- **WHEN** `new Project(id, zepId, name, startDate, endDate, billable, leistungsnachweisEnabled, leads)` is called
- **THEN** a new immutable Project instance is returned with all fields set as provided

#### Scenario: Existing project resynced from ZEP
- **WHEN** `Project.withSyncedZepData(ZepProjectProfile)` is called with updated profile data
- **THEN** a new Project instance is returned with updated name, startDate, endDate, and billable fields
- **THEN** the existing `ProjectId`, leads set, and `leistungsnachweisEnabled` flag are preserved

#### Scenario: Leistungsnachweis flag is toggled
- **WHEN** `Project.withLeistungsnachweisEnabled(false)` is called on a project whose flag is `true`
- **THEN** a new Project instance is returned with `leistungsnachweisEnabled` set to `false`
- **THEN** all other fields, including `ProjectId` and leads, are preserved

#### Scenario: MonthEndProjectSnapshot does not carry date range fields
- **WHEN** `MonthEndProjectSnapshot` is constructed
- **THEN** it contains `id`, `zepId`, `name`, `billable`, `leistungsnachweisEnabled`, and `leadIds` only
- **THEN** no `startDate` or `endDate` field exists on `MonthEndProjectSnapshot`

### Requirement: Project leads are a set of UserId references
The `leads` field SHALL be a `Set<UserId>` carried directly on the `Project` aggregate. The `Project` aggregate SHALL NOT hold raw UUID collections or references to full `User` objects. Lead replacement during project lead sync SHALL return a new Project instance with the resolved `UserId` values. Lead replacement SHALL preserve the `leistungsnachweisEnabled` flag.

#### Scenario: Project has no leads after initial sync
- **WHEN** a project is first created via `SyncProjectsUseCase`
- **THEN** its leads set is empty

#### Scenario: Leads are set by project lead sync
- **WHEN** `SyncProjectLeadsUseCase` resolves lead usernames for a project
- **THEN** the project's leads set is replaced with the resolved `UserId` values on a new Project instance
- **THEN** the `leistungsnachweisEnabled` flag is preserved on the new instance
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## ADDED Requirements

### Requirement: Project lead can list their own projects
The system SHALL expose a `GET /projects` endpoint that returns the projects for which the authenticated user is a lead. The endpoint SHALL be restricted to the `PROJECT_LEAD` role. Because the result set is scoped to the caller's own led projects, the role restriction alone is sufficient authorization for reading. The response SHALL include, for each project, its identifier, ZEP id, name, `billable` flag, and `leistungsnachweisEnabled` flag.

#### Scenario: Project lead requests their projects
- **WHEN** an authenticated `PROJECT_LEAD` user calls `GET /projects`
- **THEN** the system returns `200 OK` with the list of projects the user leads
- **THEN** each entry includes `id`, `zepId`, `name`, `billable`, and `leistungsnachweisEnabled`

#### Scenario: Project lead leads no projects
- **WHEN** an authenticated `PROJECT_LEAD` user who leads no projects calls `GET /projects`
- **THEN** the system returns `200 OK` with an empty list

#### Scenario: Non-project-lead user is rejected
- **WHEN** an authenticated user without the `PROJECT_LEAD` role calls `GET /projects`
- **THEN** the system returns `403 Forbidden`

### Requirement: Project lead can toggle Leistungsnachweis generation for a project they lead
The system SHALL expose a `PUT /projects/{projectId}/leistungsnachweis-enabled` endpoint that sets the project's `leistungsnachweisEnabled` flag from a request body carrying an `enabled` boolean. The endpoint SHALL be restricted to the `PROJECT_LEAD` role. In addition to the role restriction, the system SHALL verify that the authenticated user is a lead of the specific target project; a user who is not a lead of that project SHALL be rejected even if they hold the `PROJECT_LEAD` role. On success the flag is persisted and takes effect from the next month-end generation run.

#### Scenario: Lead disables Leistungsnachweis for their project
- **WHEN** an authenticated `PROJECT_LEAD` user who is a lead of the target project calls `PUT /projects/{projectId}/leistungsnachweis-enabled` with `enabled=false`
- **THEN** the system persists `leistungsnachweisEnabled=false` for that project
- **THEN** the system returns a success response

#### Scenario: Lead re-enables Leistungsnachweis for their project
- **WHEN** an authenticated `PROJECT_LEAD` user who is a lead of the target project calls the endpoint with `enabled=true`
- **THEN** the system persists `leistungsnachweisEnabled=true` for that project

#### Scenario: Lead of a different project is rejected
- **WHEN** an authenticated `PROJECT_LEAD` user who is NOT a lead of the target project calls the endpoint
- **THEN** the system rejects the request with an authorization error
- **THEN** the project's `leistungsnachweisEnabled` flag is unchanged

#### Scenario: Non-project-lead user is rejected
- **WHEN** an authenticated user without the `PROJECT_LEAD` role calls the endpoint
- **THEN** the system returns `403 Forbidden`

#### Scenario: Unknown project identifier
- **WHEN** the `projectId` does not correspond to an existing project
- **THEN** the system returns a not-found error
Loading