Skip to content

feat(operations): add read-only cases API foundation#386

Open
Mix13131 wants to merge 1 commit into
Worklenz:mainfrom
Mix13131:feat/operations-cases-foundation
Open

feat(operations): add read-only cases API foundation#386
Mix13131 wants to merge 1 commit into
Worklenz:mainfrom
Mix13131:feat/operations-cases-foundation

Conversation

@Mix13131

@Mix13131 Mix13131 commented Jun 23, 2026

Copy link
Copy Markdown

Summary

Adds a minimal read-only Operations Cases foundation.

What changed

  • Added read-only GET /cases.
  • Added database foundation for operational cases and next actions.
  • Added frontend operationsApiService.getCases().
  • Added minimal read-only operations case DTO/types.

Safety

  • Read-only only.
  • No create/update/delete/complete/comment endpoints.
  • No frontend write methods.
  • No Notion sync.
  • No ops memory.
  • No dashboard changes.
  • No mind map UI changes.
  • Cases are scoped by team_id from authenticated user context.

QA

  • Frontend production build: pass.
  • Backend TypeScript compile: pass.
  • git diff --check: pass.
  • Unauthenticated /api/v1/cases: returns 401 and no data.
  • Authenticated browser session confirmed endpoint returns cases.

Known notes

  • Global unauth error response currently returns message: "Internal Server Error" with 401 from the shared error handler. This is existing auth/error-response debt, not specific to /cases.
  • Current scoping is team-level: WHERE oc.team_id = req.user.team_id.
  • Project/member-level scoping is not included in this foundation PR.

Summary by CodeRabbit

  • New Features
    • Introduced Operational Cases feature enabling teams to create and manage operational cases with type and status categorization, financial impact tracking, order number assignment, and next action planning with due dates.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Three PostgreSQL migration files establish operational_cases, counterparties, assets, and money_impacts tables with ENUMs and indexes. A new CasesController implements a paginated, filterable GET endpoint mounted at /cases. Frontend TypeScript types and an operationsApiService.getCases method are added to match.

Changes

Operational Cases Feature

Layer / File(s) Summary
Core DB schema: enums, tables, and indexes
worklenz-backend/database/migrations/20260615000000-operational-cases.sql
Creates operational_case_type and operational_case_status ENUMs, an idempotent pg_temp.add_constraint helper, and the four core tables (counterparties, assets, operational_cases, money_impacts) with primary keys, foreign keys, and team-scoped indexes.
Additive column migrations
worklenz-backend/database/migrations/20260616000000-operational-case-next-action.sql, worklenz-backend/database/migrations/20260619000000-operational-order-number.sql
Adds next_action_text, next_action_due_date, and order_number columns to operational_cases with supporting indexes, all guarded by IF NOT EXISTS.
Frontend TypeScript types
worklenz-frontend/src/types/operations/operations.types.ts
Defines OperationalCaseType, OperationalCaseStatus unions, IMoneyImpact, IOperationalCase, and IOperationalCasesResponse interfaces consumed by the API service.
Backend controller and routing
worklenz-backend/src/controllers/cases-controller.ts, worklenz-backend/src/routes/apis/cases-api-router.ts, worklenz-backend/src/routes/apis/index.ts
Implements CasesController.get with filter whitelists, dynamic parameterized SQL with joins across all case-related tables, and pagination/sorting. Mounts the controller via casesApiRouter at /cases.
Frontend API service
worklenz-frontend/src/api/operations/operations.api.service.ts
Adds operationsApiService.getCases that builds a filtered query string (skipping 'all' and unset values) and issues a typed GET to /cases.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Frontend Client
  participant opsService as operationsApiService
  participant Express as casesApiRouter (Express)
  participant Controller as CasesController
  participant DB as PostgreSQL

  Client->>opsService: getCases({ case_type, status, search })
  opsService->>opsService: build URLSearchParams, skip 'all'/unset
  opsService->>Express: GET /cases?case_type=&status=
  Express->>Controller: safeControllerFunction → get(req, res)
  Controller->>Controller: whitelist sortField, build WHERE clauses
  Controller->>DB: parameterized SELECT with JOINs + jsonb_build_object
  DB-->>Controller: rows { cases[], total }
  Controller-->>Express: res.status(200) ServerResponse
  Express-->>opsService: IServerResponse<IOperationalCasesResponse>
  opsService-->>Client: response.data
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A new case has hopped onto the scene,
With ENUMs and tables all tidy and clean.
The counterpart waits, the asset's in place,
Money impacts accounted with paginèd grace.
From migration to service, the rabbit ran fast—
Operational order, built neatly to last! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(operations): add read-only cases API foundation' accurately summarizes the main change—adding a foundational read-only API for operational cases with supporting database schema and frontend types.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CLAassistant

CLAassistant commented Jun 23, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Mix13131

Copy link
Copy Markdown
Author

Implementation note for reviewers:

This PR intentionally includes only the read-only foundation for operational cases.

Out of scope:

  • mind map UI;
  • operations dashboard;
  • navbar entrypoint;
  • create/update/complete/comment endpoints;
  • Notion sync;
  • ops memory;
  • counterparty/assets writes.

Those should be reviewed in separate PRs.

@Mix13131 Mix13131 marked this pull request as ready for review June 23, 2026 08:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@worklenz-backend/database/migrations/20260615000000-operational-cases.sql`:
- Around line 96-97: The foreign key constraints at lines 96-97 for
counterparty_id and asset_id only reference the id column in their respective
tables, creating a tenant isolation vulnerability where a case can reference
entities from another team. To fix this, modify the add_constraint calls for
both operational_cases_counterparty_id_fk and operational_cases_asset_id_fk to
include team_id in the foreign key constraint, ensuring that references are
scoped to the same team by matching both team_id and id columns in the
referenced tables (counterparties and assets).

In `@worklenz-backend/src/controllers/cases-controller.ts`:
- Around line 105-106: The ORDER BY clause at lines 105-106 sorts only by the
orderBy variable, which causes non-deterministic ordering when multiple rows
share the same sort value. This can result in duplicate or missing records
across paginated results due to shifting page boundaries. Add a deterministic
tie-breaker by appending a unique identifier column (typically id) as a
secondary sort criterion to the ORDER BY clause, ensuring consistent ordering
regardless of the primary sort field's values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86603257-585d-4b66-85b3-0f341004a1bf

📥 Commits

Reviewing files that changed from the base of the PR and between c4c3268 and dbd1953.

📒 Files selected for processing (8)
  • worklenz-backend/database/migrations/20260615000000-operational-cases.sql
  • worklenz-backend/database/migrations/20260616000000-operational-case-next-action.sql
  • worklenz-backend/database/migrations/20260619000000-operational-order-number.sql
  • worklenz-backend/src/controllers/cases-controller.ts
  • worklenz-backend/src/routes/apis/cases-api-router.ts
  • worklenz-backend/src/routes/apis/index.ts
  • worklenz-frontend/src/api/operations/operations.api.service.ts
  • worklenz-frontend/src/types/operations/operations.types.ts

Comment on lines +96 to +97
CALL pg_temp.add_constraint('operational_cases', 'operational_cases_counterparty_id_fk', 'FOREIGN KEY (counterparty_id) REFERENCES counterparties ON DELETE SET NULL');
CALL pg_temp.add_constraint('operational_cases', 'operational_cases_asset_id_fk', 'FOREIGN KEY (asset_id) REFERENCES assets ON DELETE SET NULL');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce tenant-safe foreign keys for related entities.

At Line 96-97, counterparty_id and asset_id only reference id, so a case can point to another team’s records. The read query later joins on both id and team_id (worklenz-backend/src/controllers/cases-controller.ts Line 101-103), which hides the mismatch as nulls instead of preventing bad writes.

Suggested schema fix
-- counterparties / assets: add composite uniqueness to support composite FK
+CREATE UNIQUE INDEX IF NOT EXISTS counterparties_id_team_uindex
+    ON counterparties (id, team_id);
+
+CREATE UNIQUE INDEX IF NOT EXISTS assets_id_team_uindex
+    ON assets (id, team_id);

-- operational_cases: replace single-column FKs with tenant-safe composite FKs
-CALL pg_temp.add_constraint('operational_cases', 'operational_cases_counterparty_id_fk', 'FOREIGN KEY (counterparty_id) REFERENCES counterparties ON DELETE SET NULL');
-CALL pg_temp.add_constraint('operational_cases', 'operational_cases_asset_id_fk', 'FOREIGN KEY (asset_id) REFERENCES assets ON DELETE SET NULL');
+CALL pg_temp.add_constraint(
+  'operational_cases',
+  'operational_cases_counterparty_team_fk',
+  'FOREIGN KEY (counterparty_id, team_id) REFERENCES counterparties(id, team_id) ON DELETE SET NULL'
+);
+CALL pg_temp.add_constraint(
+  'operational_cases',
+  'operational_cases_asset_team_fk',
+  'FOREIGN KEY (asset_id, team_id) REFERENCES assets(id, team_id) ON DELETE SET NULL'
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@worklenz-backend/database/migrations/20260615000000-operational-cases.sql`
around lines 96 - 97, The foreign key constraints at lines 96-97 for
counterparty_id and asset_id only reference the id column in their respective
tables, creating a tenant isolation vulnerability where a case can reference
entities from another team. To fix this, modify the add_constraint calls for
both operational_cases_counterparty_id_fk and operational_cases_asset_id_fk to
include team_id in the foreign key constraint, ensuring that references are
scoped to the same team by matching both team_id and id columns in the
referenced tables (counterparties and assets).

Comment on lines +105 to +106
ORDER BY ${orderBy} ${sortOrder}
LIMIT $2 OFFSET $3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a deterministic tie-breaker to paginated sorting.

At Line 105-106, offset pagination sorts only by the selected field. When many rows share that value, page boundaries can shift and return duplicates/missing records.

Suggested fix
-              ORDER BY ${orderBy} ${sortOrder}
+              ORDER BY ${orderBy} ${sortOrder}, oc.id ${sortOrder}
               LIMIT $2 OFFSET $3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@worklenz-backend/src/controllers/cases-controller.ts` around lines 105 - 106,
The ORDER BY clause at lines 105-106 sorts only by the orderBy variable, which
causes non-deterministic ordering when multiple rows share the same sort value.
This can result in duplicate or missing records across paginated results due to
shifting page boundaries. Add a deterministic tie-breaker by appending a unique
identifier column (typically id) as a secondary sort criterion to the ORDER BY
clause, ensuring consistent ordering regardless of the primary sort field's
values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants