Departments V2 DTO Reuse Refactor Report
Summary
This report documents the current v2 department DTO duplication in services/amm/agent-web-api and proposes a refactor so that department shape changes propagate consistently across endpoints that are intended to share the same contract.
Goal: if we change the canonical shape of Department or DepartmentRef, those changes should automatically be reflected everywhere those same DTOs are used.
For this report, the docs are the source of truth for which endpoints are supposed to share the same DTO contract.
Scope Reviewed
services/amm/agent-web-api/src/api/v2/departments/departments-dtos.ts#L102(DepartmentDto)services/amm/agent-web-api/src/api/v2/departments/departments-dtos.ts#L143(DepartmentDetailDto)services/amm/agent-web-api/src/api/v2/departments/departments.controller.ts#L58(toDepartmentDto)services/amm/agent-web-api/src/api/v2/departments/departments.controller.ts#L68(toDepartmentDetailDto)services/amm/agent-web-api/src/api/v2/users/users-dtos.ts#L158(UserDepartmentRefDto)services/amm/agent-web-api/src/api/v2/users/users.controller.ts#L55(toDepartmentRefDto)services/amm/agent-web-api/src/api/v2/analytics-apps/app-utilization/utilization-dimension-dtos.ts#L106(DepartmentRefDto)services/amm/agent-web-api/src/api/v2/analytics-apps/app-utilization/app-utilization.controller.ts#L862(inline department mapping)
Findings
1. The docs define one Department DTO, but the backend splits it into two inconsistent classes
The docs define a single Department shape used by list, detail, create, and update responses:
/departments— DTO definition/departments/list— returnsDepartment[]/departments/details— returnsDepartment/departments/create— returnsDepartment/departments/update— returnsDepartment
The backend implements this as two separate classes that do not share any inheritance or composition:
DepartmentDtoinservices/amm/agent-web-api/src/api/v2/departments/departments-dtos.ts#L102DepartmentDetailDtoinservices/amm/agent-web-api/src/api/v2/departments/departments-dtos.ts#L143
DepartmentDetailDto is missing members_count, so detail, create, and update responses do not include the member count that the docs require.
The controller reinforces this split with two separate mapper functions:
// departments.controller.ts#L58 — list mapper (has members_count)
const toDepartmentDto = (dept: any, membersCount: number): DepartmentDto => ({
id: dept.id,
client_id: dept.clientId,
name: dept.name,
description: dept.description ?? null,
members_count: membersCount,
created_at: dept.createdAt instanceof Date ? dept.createdAt.toISOString() : dept.createdAt,
updated_at: dept.updatedAt instanceof Date ? dept.updatedAt.toISOString() : dept.updatedAt,
});
// departments.controller.ts#L68 — detail mapper (no members_count)
const toDepartmentDetailDto = (dept: any): DepartmentDetailDto => ({
id: dept.id,
client_id: dept.clientId,
name: dept.name,
description: dept.description ?? null,
created_at: dept.createdAt instanceof Date ? dept.createdAt.toISOString() : dept.createdAt,
updated_at: dept.updatedAt instanceof Date ? dept.updatedAt.toISOString() : dept.updatedAt,
});
Adding a field to the documented Department shape requires updating both classes and both mappers, and the compiler will not flag a missed one.
2. DepartmentRef is duplicated across two modules
The docs define DepartmentRef as a minimal reference shape (id, name, description) for embedding in other domains:
/departments— DTO definition- Embedded as
User.departments[]— see Users domain - Embedded in analytics utilization responses
The backend has two independent classes for this:
UserDepartmentRefDtoinservices/amm/agent-web-api/src/api/v2/users/users-dtos.ts#L158DepartmentRefDtoinservices/amm/agent-web-api/src/api/v2/analytics-apps/app-utilization/utilization-dimension-dtos.ts#L106
These already have a bug divergence. UserDepartmentRefDto is missing type: String in its @ApiProperty decorator for description:
// users-dtos.ts#L168 — INCORRECT: missing type: String
@ApiProperty({ nullable: true, description: 'Optional department description' })
description!: string | null;
// utilization-dimension-dtos.ts#L113 — CORRECT: has type: String
@ApiProperty({ nullable: true, type: String, description: 'Department description' })
description!: string | null;
This causes the OpenAPI spec to type UserDepartmentRefDto.description as nullable object instead of nullable string. The analytics DepartmentRefDto is correct. Fixing one does not fix the other.
3. The analytics controller maps department refs inline instead of using a shared mapper
The analytics controller builds DepartmentRefDto objects with inline object literals in two places:
// app-utilization.controller.ts#L862 — user utilization
department: item.department
? { id: item.department.id, name: item.department.name, description: item.department.description }
: null,
// app-utilization.controller.ts#L945 — department utilization
department: { id: item.department.id, name: item.department.name, description: item.department.description },
The users controller has its own mapper function:
// users.controller.ts#L55
const toDepartmentRefDto = (dept: UserDepartment): UserDepartmentRefDto => ({
id: dept.id,
name: dept.name,
description: dept.description ?? null,
});
There is no shared mapper. Each consumer independently maps the same three fields.
Current Duplication Map
DepartmentRef (id, name, description)
| Purpose | Class | Location | Has bug? |
|---|---|---|---|
Embedded in UserDto.departments | UserDepartmentRefDto | api/v2/users/users-dtos.ts#L158 | Yes — description typed as object in OpenAPI |
| Embedded in analytics utilization | DepartmentRefDto | api/v2/analytics-apps/app-utilization/utilization-dimension-dtos.ts#L106 | No |
Department (id, name, description, member_count, created_at, updated_at)
| Purpose | Class | Location | Has member_count? |
|---|---|---|---|
| List response items | DepartmentDto | api/v2/departments/departments-dtos.ts#L102 | Yes (as members_count) |
| Detail / Create / Update response | DepartmentDetailDto | api/v2/departments/departments-dtos.ts#L143 | No — missing |
Mapper functions
| Purpose | Function | Location | Shared? |
|---|---|---|---|
| Department list item | toDepartmentDto | departments.controller.ts#L58 | No — controller-local |
| Department detail | toDepartmentDetailDto | departments.controller.ts#L68 | No — controller-local |
| User department ref | toDepartmentRefDto | users.controller.ts#L55 | No — controller-local |
| Analytics department ref | (inline) | app-utilization.controller.ts#L862,945 | No — inline literals |
Refactor Goal
Introduce canonical shared DTO classes for department resources and use them anywhere the docs say the same resource shape should be returned or embedded.
The rule should be:
- if the docs say an endpoint returns
Department, use the same sharedDepartmentDto - if the docs say an endpoint embeds
DepartmentRef, use the same sharedDepartmentRefDto - if an endpoint intentionally returns a different projection, give it a distinct name and do not pretend it is the canonical resource
Proposed Target Design
1. Create shared canonical DTOs in one module
Create one shared DTO file for reusable department resource contracts:
src/api/v2/departments/shared/department-resource-dtos.ts
Recommended canonical classes:
DepartmentRefDto— the minimal reference (id, name, description) for embeddingDepartmentDto— the full department shape withmember_countand timestamps
2. Collapse DepartmentDto and DepartmentDetailDto into one class
The docs define one Department shape. The backend should have one class.
The current DepartmentDetailDto is a strict subset of DepartmentDto — it has the same fields minus members_count. Since the docs require member_count on all department-primary endpoints, DepartmentDetailDto should be eliminated and all endpoints should use the unified DepartmentDto.
This means the detail, create, and update endpoints need to fetch or compute the member count to populate the field. For create, the count is always 0.
3. Reuse DepartmentRefDto across modules
Replace module-local copies with the shared class:
UserDepartmentRefDtoin users → import sharedDepartmentRefDtoDepartmentRefDtoin analytics → import sharedDepartmentRefDto
This automatically fixes the description typing bug in UserDepartmentRefDto since both modules will use the same class with the correct @ApiProperty({ nullable: true, type: String }) decorator.
4. Move mapping into shared department mappers
Create shared mapper functions:
src/api/v2/departments/shared/department-mappers.ts
Recommended functions:
toDepartmentDto(dept, memberCount)— maps entity + count to fullDepartmentDtotoDepartmentRefDto(dept)— maps entity to minimalDepartmentRefDto
Then reuse those mappers from:
- departments controller (list, detail, create, update)
- users controller (embedded
departments[]inUserDto) - analytics controller (embedded
departmentin utilization items)
Recommended Rollout
Phase 1. Establish canonical resource DTOs
- Create
src/api/v2/departments/shared/department-resource-dtos.tswithDepartmentRefDtoandDepartmentDto - Create
src/api/v2/departments/shared/department-mappers.tswithtoDepartmentDtoandtoDepartmentRefDto - Keep existing DTOs temporarily while migrating imports
Phase 2. Collapse DepartmentDetailDto into DepartmentDto
- Remove
DepartmentDetailDtoclass - Update detail, create, and update endpoints to return
DepartmentDto(withmember_count) - Update controller to use a single
toDepartmentDtomapper - For create: pass
0as member count - For detail and update: fetch the member count from the repository
Phase 3. Migrate cross-module references
- Replace
UserDepartmentRefDtoinusers-dtos.tswith an import of sharedDepartmentRefDto - Replace
DepartmentRefDtoinutilization-dimension-dtos.tswith an import of sharedDepartmentRefDto - Replace inline department mapping in analytics controller with shared
toDepartmentRefDto - Replace
toDepartmentRefDtoin users controller with shared mapper
Phase 4. Regenerate and verify the contract
- Regenerate
services/amm/agent-web-api/openapi.json - Confirm the OpenAPI no longer emits duplicate schemas for the same department shape
- Confirm
UserDepartmentRefDto/DepartmentRefDtoschema consolidation in OpenAPI output - Rerun any contract tests
Acceptance Criteria
The refactor is complete when all of the following are true:
- Changing the shared
DepartmentDtoupdates every endpoint that is documented to returnDepartment(list, detail, create, update) - Changing the shared
DepartmentRefDtoupdates every endpoint that embedsDepartmentRef(users, analytics) DepartmentDetailDtono longer exists — all department-primary endpoints use the singleDepartmentDto- The users module does not define
UserDepartmentRefDto— it imports the sharedDepartmentRefDto - The analytics module does not define its own
DepartmentRefDto— it imports the shared one - Shared mapper functions are used instead of controller-local or inline mapping blocks
- The
descriptionfield is correctly typed as nullablestring(notobject) in all OpenAPI schemas that reference department ref
Risks
Detail/create/update endpoints need to fetch member count
Currently DepartmentDetailDto avoids the member count query. Collapsing into DepartmentDto means the detail, create, and update endpoints must either:
- Run an additional count query, or
- Use a repository method that returns the department with its count (like
listByClientV2already does for list items)
The list endpoint already computes this count efficiently. The same pattern can be reused for single-department fetches.
Breaking generated clients
Removing DepartmentDetailDto will rename the schema used by detail/create/update responses in OpenAPI. Generated SDKs that reference the old schema name will need to be regenerated.
OpenAPI schema consolidation
When UserDepartmentRefDto and DepartmentRefDto are replaced by a single shared class, the OpenAPI generator will emit one schema instead of two. Consumers referencing either old name will see it disappear.
Requested Action
Implement a shared department DTO layer for v2, collapse DepartmentDetailDto into DepartmentDto, migrate users and analytics endpoints to reuse the shared DepartmentRefDto, and extract shared mapper functions to eliminate inline and controller-local mapping duplication.