Skip to content

TEC-25: QuickWerk Phase 4 operator dispute lifecycle (end-to-end)#13

Merged
imKXNNY merged 6 commits into
mainfrom
codexcoder/tec-25-operator-dispute-lifecycle
Apr 12, 2026
Merged

TEC-25: QuickWerk Phase 4 operator dispute lifecycle (end-to-end)#13
imKXNNY merged 6 commits into
mainfrom
codexcoder/tec-25-operator-dispute-lifecycle

Conversation

@imKXNNY

@imKXNNY imKXNNY commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add dispute operator action contract + guards (startReview, resolve, close) in domain and API client
  • add authenticated operator PATCH /api/v1/disputes/:disputeId/start-review|resolve|close endpoints with deterministic transition/idempotency behavior
  • add repository transition API + in-memory implementation + postgres dispute adapter and migration 0007_disputes_lifecycle.sql
  • wire admin-web dispute queue actions/reducers with optimistic transition + rollback handling
  • add focused tests and dispute flow smoke script

Validation

  • pnpm --filter @quickwerk/platform-api test -- src/disputes/disputes.service.test.ts src/disputes/infrastructure/in-memory-dispute.repository.test.ts src/disputes/infrastructure/dispute-repository.provider.test.ts
  • pnpm --filter @quickwerk/admin-web test -- src/features/disputes/dispute-queue-state.test.ts src/features/disputes/dispute-queue-actions.test.ts
  • pnpm --filter @quickwerk/platform-api typecheck
  • pnpm --filter @quickwerk/admin-web typecheck
  • pnpm --filter @quickwerk/domain typecheck
  • pnpm --filter @quickwerk/api-client typecheck

Notes

  • smoke flow script added at scripts/smoke/operator-dispute-flow-smoke.sh.

Summary by CodeRabbit

  • New Features

    • Operators can start review, resolve, and close disputes via new API actions; pending queue now shows under-review items and explicit action state (idle/transitioning/done/error).
    • Disputes are persisted in a new database table; added an end-to-end operator dispute smoke test.
  • Bug Fixes

    • Stronger validation and clearer error responses for invalid transitions; optimistic UI updates now roll back cleanly on failure.
  • Tests

    • Expanded unit, integration, and repository tests covering transitions, idempotency, authorization, and persistence.
  • Documentation

    • Migration README updated with dispute migration and run instructions.

Implement operator dispute flow (start-review, resolve, close) across domain contract, API client, platform API, persistence adapters, admin queue reducers/actions, tests, and smoke script.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds a full disputes lifecycle: DB migration and Postgres repository, repo resolver, service/controller operator transitions (startReview/resolve/close) with validation and guards, API client routes, frontend optimistic transition flows and state helpers, tests across layers, and a smoke script.

Changes

Cohort / File(s) Summary
Frontend: queue state & tests
apps/admin-web/src/features/disputes/dispute-queue-state.ts, apps/admin-web/src/features/disputes/dispute-queue-state.test.ts
Extend loaded state with queueAction; add DisputeQueueActionType/DisputeQueueAction; implement beginOptimisticDisputeTransition, applyDisputeTransitionSuccess, rollbackDisputeTransition; update tests for idle/transition/error/done.
Frontend: actions & tests
apps/admin-web/src/features/disputes/dispute-queue-actions.ts, apps/admin-web/src/features/disputes/dispute-queue-actions.test.ts
Add request builders selection and requestDisputeTransition; export submitDisputeTransition coordinating optimistic update, HTTP PATCH calls, error extraction, success application and rollback; add tests for success and 409 rollback.
Domain types & utilities
packages/domain/src/index.ts
Add DisputeOperatorActionType (`startReview
API client
packages/api-client/src/index.ts
Add dispute API route keys and request builders: createStartReviewDisputeRequest, createResolveDisputeRequest, createCloseDisputeRequest (PATCH routes, auth and JSON body handling).
DB migration & docs
services/platform-api/migrations/0007_disputes_lifecycle.sql, services/platform-api/migrations/README.md
Add disputes table with lifecycle columns, constraints, uniqueness/consistency checks, and indexes; update migrations README ordering and manual run steps.
Repository interface & provider
services/platform-api/src/disputes/domain/dispute.repository.ts, services/platform-api/src/disputes/infrastructure/dispute-repository.provider.ts, services/platform-api/src/disputes/infrastructure/dispute-repository.provider.test.ts
Extend DisputeRepository with findById, findByStatuses, transitionStatus; replace prior provider with resolveDisputeRepository factory returning in-memory or Postgres implementation; add resolver tests.
In-memory repo & tests
services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.ts, services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.test.ts
Implement findById, findByStatuses, transitionStatus with allowed-status checks, replay detection, and conditional resolvedAt/resolutionNote; add tests for success, replay, and conflict.
Postgres repo & integration
services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts, services/platform-api/src/persistence/postgres-mode.integration.test.ts
Add PostgresDisputeRepository with row mapping, UUID validation, queries including findByStatuses, and transactional transitionStatus using FOR UPDATE; extend integration tests and truncate disputes.
Service layer & tests
services/platform-api/src/disputes/disputes.service.ts, services/platform-api/src/disputes/disputes.service.test.ts
Query pending disputes for `open
Controller & module wiring
services/platform-api/src/disputes/disputes.controller.ts, services/platform-api/src/disputes/disputes.module.ts, services/platform-api/src/modules/index.ts
Add PATCH endpoints /:disputeId/start-review, /resolve, /close with correlation ID and operator session handling; replace provider registration with factory DISPUTE_REPOSITORY wiring; add 'disputes' to exported modules list.
Scripts: smoke test
scripts/smoke/operator-dispute-flow-smoke.sh
Add bash smoke script that exercises end-to-end dispute flow: sign-in, create booking, submit dispute, start-review, resolve, and verify pending list excludes resolved dispute.
Misc: TS/config
apps/admin-web/next-env.d.ts, apps/admin-web/tsconfig.json
Minor Next route types import and tsconfig adjustments; no public API surface changes.

Sequence Diagram(s)

sequenceDiagram
    participant Frontend as Admin Web UI
    participant Store as State Management
    participant API as Backend API
    participant Service as DisputesService
    participant Repo as DisputeRepository
    participant DB as PostgreSQL

    rect rgba(76, 175, 80, 0.5)
    Note over Frontend,DB: Optimistic Dispute Transition Flow
    end

    Frontend->>Store: submitDisputeTransition(disputeId, 'resolve')
    activate Store
    Store->>Store: beginOptimisticDisputeTransition()
    Store-->>Frontend: Updated state (transitioning)
    deactivate Store

    rect rgba(33, 150, 243, 0.5)
    Note over Frontend,DB: API Request Phase
    end

    Frontend->>API: PATCH /api/v1/disputes/:disputeId/resolve {resolutionNote}
    activate API
    API->>Service: resolveDispute(session, disputeId, note)
    activate Service
    Service->>Repo: transitionStatus({nextStatus:'resolved', allowedCurrentStatuses:[...]})
    activate Repo
    Repo->>DB: SELECT ... FOR UPDATE / UPDATE
    Repo-->>Service: {ok: true, dispute}
    deactivate Repo
    Service-->>API: {ok: true, dispute}
    deactivate Service
    API-->>Frontend: 200 OK, dispute
    deactivate API

    rect rgba(156, 39, 176, 0.5)
    Note over Frontend,DB: Success Path
    end

    Frontend->>Store: applyDisputeTransitionSuccess(dispute)
    activate Store
    Store-->>Frontend: Updated state (done / dispute removed)
    deactivate Store
Loading
sequenceDiagram
    participant Frontend as Admin Web UI
    participant Store as State Management
    participant API as Backend API
    participant Service as DisputesService
    participant Repo as DisputeRepository

    rect rgba(244, 67, 54, 0.5)
    Note over Frontend,Repo: Error/Rollback Flow
    end

    Frontend->>Store: submitDisputeTransition(disputeId, 'startReview')
    activate Store
    Store->>Store: beginOptimisticDisputeTransition()
    Store-->>Frontend: Updated state (under-review optimistic)
    deactivate Store

    Frontend->>API: PATCH /api/v1/disputes/:disputeId/start-review
    activate API
    API->>Service: startReviewDispute(session, disputeId)
    activate Service
    Service->>Repo: transitionStatus({nextStatus:'under-review', allowedCurrentStatuses:[...]})
    activate Repo
    Repo-->>Service: {ok:false, reason:'transition-conflict'}
    deactivate Repo
    Service-->>API: {ok:false, statusCode:409, error}
    deactivate Service
    API-->>Frontend: 409 Conflict
    deactivate API

    Frontend->>Store: rollbackDisputeTransition(previousState, error)
    activate Store
    Store-->>Frontend: Restored state with queueAction error
    deactivate Store
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through schema, service, and route,

I nudged an optimistic change, then kept it afloat.
PATCHed a dispute, then watched the state sing—
Rollbacks and resolves, a tidy little spring. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly identifies the main change: implementing the complete operator dispute lifecycle functionality (startReview, resolve, close transitions) as part of QuickWerk Phase 4, with the ticket reference TEC-25.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codexcoder/tec-25-operator-dispute-lifecycle

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (5)
services/platform-api/src/disputes/disputes.service.test.ts (1)

205-286: Consider adding a close transition test for full lifecycle coverage.

The suite validates startReview and resolve, but not close. Adding one close-path test would complete coverage for all operator actions introduced in this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/disputes/disputes.service.test.ts` around lines 205
- 286, Add a new spec in the "operator transitions" describe block that covers
the full lifecycle including the close transition: create service/bookings,
submit a dispute via submitDispute, transition it to under-review with
startReviewDispute, resolve it with resolveDispute, then call closeDispute as an
operator and assert the final status becomes "closed", resolvedAt remains set,
resolutionNote retained, and that getPendingDisputes no longer returns the
dispute; also include role/guard checks (e.g., ensure non-operator cannot close)
to mirror the existing tests' patterns using the same helper functions
(submitDispute, startReviewDispute, resolveDispute, closeDispute,
getPendingDisputes).
services/platform-api/src/disputes/infrastructure/dispute-repository.provider.test.ts (1)

21-34: Add fail-fast coverage for missing DATABASE_URL in postgres mode.

Current tests validate selection, but not the required config guard. Add one negative case so provider behavior is locked down.

✅ Suggested test addition
 describe('resolveDisputeRepository', () => {
+  it('throws when postgres mode is selected without DATABASE_URL', () => {
+    const inMemoryRepository = new InMemoryDisputeRepository();
+
+    expect(() =>
+      resolveDisputeRepository({
+        inMemoryRepository,
+        postgresClient: new PostgresClient(),
+        env: {
+          PERSISTENCE_MODE: 'postgres',
+        },
+      }),
+    ).toThrowError(/DATABASE_URL/i);
+  });
+
   it('returns postgres repository in postgres mode', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform-api/src/disputes/infrastructure/dispute-repository.provider.test.ts`
around lines 21 - 34, Add a negative test case to ensure
resolveDisputeRepository fails fast when env.PERSISTENCE_MODE is 'postgres' but
env.DATABASE_URL is missing; update the test file to call
resolveDisputeRepository with inMemoryRepository, a PostgresClient instance, and
env.PERSISTENCE_MODE set to 'postgres' but without DATABASE_URL (or set it to
empty/undefined) and assert that it throws/returns an error (or behaves as your
provider's guard does). Specifically reference resolveDisputeRepository,
PERSISTENCE_MODE, DATABASE_URL, InMemoryDisputeRepository, PostgresClient, and
PostgresDisputeRepository so the test mirrors the existing positive case but
expects the guard to trigger.
services/platform-api/src/persistence/postgres-mode.integration.test.ts (1)

147-167: Extend integration test to assert replay and conflict semantics.

The test currently verifies only the happy path. Add replay/conflict checks to validate parity with the in-memory contract and catch regression in transition rules.

✅ Suggested assertions to add
     expect(resolveTransition.ok).toBe(true);
     if (!resolveTransition.ok) return;
     expect(resolveTransition.dispute.status).toBe('resolved');
     expect(resolveTransition.dispute.resolutionNote).toContain('integration test');
+
+    const replayResolve = await disputeRepository.transitionStatus({
+      disputeId,
+      allowedCurrentStatuses: ['under-review'],
+      nextStatus: 'resolved',
+      resolvedAt: new Date().toISOString(),
+      resolutionNote: 'Resolved in integration test.',
+    });
+    expect(replayResolve.ok).toBe(true);
+    if (replayResolve.ok) {
+      expect(replayResolve.replayed).toBe(true);
+    }
+
+    const invalidClose = await disputeRepository.transitionStatus({
+      disputeId,
+      allowedCurrentStatuses: ['under-review'],
+      nextStatus: 'closed',
+    });
+    expect(invalidClose.ok).toBe(false);
+    if (!invalidClose.ok) {
+      expect(invalidClose.reason).toBe('transition-conflict');
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/platform-api/src/persistence/postgres-mode.integration.test.ts`
around lines 147 - 167, After the happy-path assertions for reviewTransition and
resolveTransition, add replay and conflict checks: call
disputeRepository.transitionStatus again with the same parameters used for
reviewTransition and assert the repeated call indicates a replay/no-op (expect
ok to be false and dispute status remains 'under-review'); then call
transitionStatus with disputeId but with an incorrect allowedCurrentStatuses
(e.g., ['open'] after it moved) and assert that this returns a conflict (expect
ok to be false and the dispute status remains unchanged); use the existing
variables disputeId, reviewTransition, resolveTransition and the
transitionStatus method names to locate where to insert these assertions.
apps/admin-web/src/features/disputes/dispute-queue-actions.test.ts (1)

84-96: Assert the request path in these transition tests.

Both stubs accept any URL, so a regression in the /resolve or /start-review route mapping would still pass. Verifying the expected path here would make the new coverage exercise the client wiring, not just the reducer behavior.

Also applies to: 113-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-web/src/features/disputes/dispute-queue-actions.test.ts` around
lines 84 - 96, The fetch stub (fetchImpl) in the dispute transition tests only
asserts the HTTP method and ignores the request URL, so add an assertion that
the incoming _url matches the expected route for the action under test (e.g.,
the resolve test should assert the path includes the dispute id and "/resolve",
and likewise the start-review test should assert the path includes
"/start-review"). Update the fetchImpl used in those tests (the one referencing
makeDispute('d-1')) to call expect(_url).toBe(...) or expect(_url).toMatch(...)
with the exact API route you expect, and apply the same URL assertion change to
the other stub used around lines 113-118 so both transition tests validate the
client routing as well as method and response.
apps/admin-web/src/features/disputes/dispute-queue-state.ts (1)

3-9: Reuse the domain transition helpers for optimistic updates.

This file now hardcodes both the action union and the action → status mapping even though packages/domain/src/index.ts:312-327 already owns those rules. Importing the domain types/helpers here would keep the optimistic UI in lockstep with the backend and avoid showing impossible transient states if the workflow changes.

Also applies to: 26-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-web/src/features/disputes/dispute-queue-state.ts` around lines 3 -
9, Replace the hardcoded DisputeQueueActionType and DisputeQueueAction
definitions with the canonical types/helpers from the domain package: remove
DisputeQueueActionType and the local DisputeQueueAction union and import the
domain-owned action type and transition helper (the types/helpers referenced in
packages/domain that drive action→status transitions) and use them for
optimistic UI state and mapping; update any code that previously relied on
DisputeQueueActionType/DisputeQueueAction to use the imported domain type names
and the domain transition helper so the optimistic states follow the
backend-owned rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/api-client/src/index.ts`:
- Around line 347-356: The helper createResolveDisputeRequest currently requires
body: { resolutionNote: string } while the API allows resolutionNote to be
optional; change the body type to { resolutionNote?: string } and ensure any
code using createResolveDisputeRequest still works with an optional
resolutionNote (no other signature changes needed), keeping the returned object
shape and const assertions the same.

In `@services/platform-api/src/disputes/disputes.service.test.ts`:
- Around line 157-177: The test currently doesn't verify that startReviewDispute
actually succeeded or that an "under-review" dispute is returned; update the
test to assert the startReviewDispute call (the result of
service.startReviewDispute) is ok (throw or expect) and then tighten the final
assertions on service.getPendingDisputes to require that at least one dispute
has status === 'under-review' rather than only checking every dispute is 'open'
|| 'under-review'; reference submitDispute, startReviewDispute, and
getPendingDisputes to locate the calls and ensure the operator session is used
for the review action.

In `@services/platform-api/src/disputes/disputes.service.ts`:
- Around line 154-161: The current code calls this.disputes.findByStatus twice
(producing openDisputes and underReviewDisputes) and then merges/sorts them,
which can race and double-count if a status changes mid-flight; change to a
single repository read that queries both statuses at once (e.g., add/use a
method like this.disputes.findByStatuses(['open','under-review'], { orderBy:
'createdAt' }) or a single find({ status: IN(...) , order: 'createdAt' })) and
replace the Promise.all + merge/sort logic with that single call so the DB
returns a consistent ordered snapshot. Ensure you update the Disputes repository
(method name used above) and call site (replace openDisputes/underReviewDisputes
and the combined sort) accordingly.
- Around line 174-195: The service calls .trim() on resolutionNote without
runtime type checks, causing a TypeError if a non-string value is passed; update
resolveDispute and closeDispute to first verify typeof resolutionNote ===
'string' (for resolveDispute reject non-strings with a 400 and message like
"resolutionNote must be a string and non-empty") and for closeDispute treat
non-string/undefined as null or return 400 for explicit non-string inputs per
your API contract, then call .trim() only after confirming it's a string and
pass the trimmed string (or null) into transitionDispute (reference functions:
resolveDispute, closeDispute, transitionDispute).

In
`@services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts`:
- Around line 26-67: The save method currently ignores the query result so
INSERT ... ON CONFLICT DO NOTHING can silently fail; update the save(dispute:
DisputeRecord) implementation to inspect the result from
this.postgresClient.query (check result.rowCount) and if rowCount === 0 return {
ok: false } (or alternatively fetch and return the existing dispute row) instead
of always returning { ok: true }; reference the save function and the
postgresClient.query call and ensure the caller in disputes.service.ts can
detect insertion failure via saved.ok.

---

Nitpick comments:
In `@apps/admin-web/src/features/disputes/dispute-queue-actions.test.ts`:
- Around line 84-96: The fetch stub (fetchImpl) in the dispute transition tests
only asserts the HTTP method and ignores the request URL, so add an assertion
that the incoming _url matches the expected route for the action under test
(e.g., the resolve test should assert the path includes the dispute id and
"/resolve", and likewise the start-review test should assert the path includes
"/start-review"). Update the fetchImpl used in those tests (the one referencing
makeDispute('d-1')) to call expect(_url).toBe(...) or expect(_url).toMatch(...)
with the exact API route you expect, and apply the same URL assertion change to
the other stub used around lines 113-118 so both transition tests validate the
client routing as well as method and response.

In `@apps/admin-web/src/features/disputes/dispute-queue-state.ts`:
- Around line 3-9: Replace the hardcoded DisputeQueueActionType and
DisputeQueueAction definitions with the canonical types/helpers from the domain
package: remove DisputeQueueActionType and the local DisputeQueueAction union
and import the domain-owned action type and transition helper (the types/helpers
referenced in packages/domain that drive action→status transitions) and use them
for optimistic UI state and mapping; update any code that previously relied on
DisputeQueueActionType/DisputeQueueAction to use the imported domain type names
and the domain transition helper so the optimistic states follow the
backend-owned rules.

In `@services/platform-api/src/disputes/disputes.service.test.ts`:
- Around line 205-286: Add a new spec in the "operator transitions" describe
block that covers the full lifecycle including the close transition: create
service/bookings, submit a dispute via submitDispute, transition it to
under-review with startReviewDispute, resolve it with resolveDispute, then call
closeDispute as an operator and assert the final status becomes "closed",
resolvedAt remains set, resolutionNote retained, and that getPendingDisputes no
longer returns the dispute; also include role/guard checks (e.g., ensure
non-operator cannot close) to mirror the existing tests' patterns using the same
helper functions (submitDispute, startReviewDispute, resolveDispute,
closeDispute, getPendingDisputes).

In
`@services/platform-api/src/disputes/infrastructure/dispute-repository.provider.test.ts`:
- Around line 21-34: Add a negative test case to ensure resolveDisputeRepository
fails fast when env.PERSISTENCE_MODE is 'postgres' but env.DATABASE_URL is
missing; update the test file to call resolveDisputeRepository with
inMemoryRepository, a PostgresClient instance, and env.PERSISTENCE_MODE set to
'postgres' but without DATABASE_URL (or set it to empty/undefined) and assert
that it throws/returns an error (or behaves as your provider's guard does).
Specifically reference resolveDisputeRepository, PERSISTENCE_MODE, DATABASE_URL,
InMemoryDisputeRepository, PostgresClient, and PostgresDisputeRepository so the
test mirrors the existing positive case but expects the guard to trigger.

In `@services/platform-api/src/persistence/postgres-mode.integration.test.ts`:
- Around line 147-167: After the happy-path assertions for reviewTransition and
resolveTransition, add replay and conflict checks: call
disputeRepository.transitionStatus again with the same parameters used for
reviewTransition and assert the repeated call indicates a replay/no-op (expect
ok to be false and dispute status remains 'under-review'); then call
transitionStatus with disputeId but with an incorrect allowedCurrentStatuses
(e.g., ['open'] after it moved) and assert that this returns a conflict (expect
ok to be false and the dispute status remains unchanged); use the existing
variables disputeId, reviewTransition, resolveTransition and the
transitionStatus method names to locate where to insert these assertions.
🪄 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: d1d8389d-6621-4c80-8523-95d84ca12dec

📥 Commits

Reviewing files that changed from the base of the PR and between d0abebd and c3e5682.

📒 Files selected for processing (21)
  • apps/admin-web/src/features/disputes/dispute-queue-actions.test.ts
  • apps/admin-web/src/features/disputes/dispute-queue-actions.ts
  • apps/admin-web/src/features/disputes/dispute-queue-state.test.ts
  • apps/admin-web/src/features/disputes/dispute-queue-state.ts
  • packages/api-client/src/index.ts
  • packages/domain/src/index.ts
  • scripts/smoke/operator-dispute-flow-smoke.sh
  • services/platform-api/migrations/0007_disputes_lifecycle.sql
  • services/platform-api/migrations/README.md
  • services/platform-api/src/disputes/disputes.controller.ts
  • services/platform-api/src/disputes/disputes.module.ts
  • services/platform-api/src/disputes/disputes.service.test.ts
  • services/platform-api/src/disputes/disputes.service.ts
  • services/platform-api/src/disputes/domain/dispute.repository.ts
  • services/platform-api/src/disputes/infrastructure/dispute-repository.provider.test.ts
  • services/platform-api/src/disputes/infrastructure/dispute-repository.provider.ts
  • services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.test.ts
  • services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.ts
  • services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts
  • services/platform-api/src/modules/index.ts
  • services/platform-api/src/persistence/postgres-mode.integration.test.ts

Comment thread packages/api-client/src/index.ts
Comment thread services/platform-api/src/disputes/disputes.service.test.ts
Comment thread services/platform-api/src/disputes/disputes.service.ts Outdated
Comment thread services/platform-api/src/disputes/disputes.service.ts
Co-Authored-By: Paperclip <noreply@paperclip.ing>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/admin-web/src/features/disputes/dispute-queue-state.ts`:
- Around line 30-52: The reducer beginOptimisticDisputeTransition must
early-return instead of entering a transitioning queueAction when the target
dispute is missing, the action is invalid for the dispute's current status, or
another transition is already in-flight; update beginOptimisticDisputeTransition
to: 1) check state.status === 'loaded' (keep existing), 2) return state if
state.queueAction?.status === 'transitioning', 3) find the target dispute by
disputeId from state.disputes and return state if not found, 4) compute
nextStatus via disputeOperatorActionTransitions[actionType] and verify the
action is valid for the dispute's current status (e.g., mapping exists and
nextStatus differs from dispute.status); only then produce the new disputes
array with the single dispute's status set to nextStatus and set queueAction to
{ status: 'transitioning', disputeId, actionType } — otherwise return the
original state.

In `@services/platform-api/src/disputes/disputes.controller.ts`:
- Around line 23-25: The ResolveDisputeBody type currently declares
resolutionNote as optional but the service (resolve method in
disputes.service.ts) rejects missing notes; update the type alias
ResolveDisputeBody so resolutionNote is required (change resolutionNote?:
unknown to resolutionNote: unknown) and ensure any usages (e.g., the resolve
endpoint handler in disputes.controller.ts that accepts ResolveDisputeBody)
reflect this stricter contract; leave close as optional if present.

In
`@services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts`:
- Around line 230-239: The short-circuit that returns replayed when
current.status === input.nextStatus should also verify the resolution fields;
instead of immediately returning {replayed: true} in the block that checks
current.status === input.nextStatus, compute the intended
nextResolvedAt/nextResolutionNote (using input.resolvedAt/input.resolutionNote
falling back to current.resolved_at/current.resolution_note as the diff shows)
and only treat it as a replay if nextResolvedAt === current.resolved_at and
nextResolutionNote === current.resolution_note; if the status matches but either
resolution note or resolvedAt would change, return a conflict (e.g., the same
shape as the existing transition-conflict { ok: false, reason:
'transition-conflict', currentStatus: current.status }) so callers get a 409
instead of a silent replay—apply this change in the method in
postgres-dispute.repository.ts where current, input.nextStatus,
input.resolutionNote, and input.resolvedAt are used and mapDisputeRow is
returned for successful replays.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f980746-7b54-45b4-96c6-9cc4de2e98a9

📥 Commits

Reviewing files that changed from the base of the PR and between c3e5682 and ca9eb51.

📒 Files selected for processing (11)
  • apps/admin-web/src/features/disputes/dispute-queue-actions.test.ts
  • apps/admin-web/src/features/disputes/dispute-queue-state.ts
  • packages/api-client/src/index.ts
  • services/platform-api/src/disputes/disputes.controller.ts
  • services/platform-api/src/disputes/disputes.service.test.ts
  • services/platform-api/src/disputes/disputes.service.ts
  • services/platform-api/src/disputes/domain/dispute.repository.ts
  • services/platform-api/src/disputes/infrastructure/dispute-repository.provider.test.ts
  • services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.ts
  • services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts
  • services/platform-api/src/persistence/postgres-mode.integration.test.ts
✅ Files skipped from review due to trivial changes (1)
  • services/platform-api/src/disputes/disputes.service.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • services/platform-api/src/disputes/infrastructure/dispute-repository.provider.test.ts
  • apps/admin-web/src/features/disputes/dispute-queue-actions.test.ts
  • services/platform-api/src/persistence/postgres-mode.integration.test.ts
  • services/platform-api/src/disputes/domain/dispute.repository.ts
  • services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.ts

Comment thread apps/admin-web/src/features/disputes/dispute-queue-state.ts
Comment thread services/platform-api/src/disputes/disputes.controller.ts
Comment thread services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts Outdated
@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 5 file(s) based on 3 unresolved review comments.

Files modified:

  • apps/admin-web/next-env.d.ts
  • apps/admin-web/src/features/disputes/dispute-queue-state.ts
  • apps/admin-web/tsconfig.json
  • services/platform-api/src/disputes/disputes.controller.ts
  • services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts

Commit: 75c0acf93a6d4da61f8f7ff0dc27e59d4a6dcda1

The changes have been pushed to the codexcoder/tec-25-operator-dispute-lifecycle branch.

Time taken: 3m 47s

Fixed 5 file(s) based on 3 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/admin-web/src/features/disputes/dispute-queue-state.ts (1)

1-5: ⚠️ Potential issue | 🟠 Major

Still missing allowed-from-status validation before optimistic transition.

At Line 48-50, checking only nextStatus !== dispute.status is insufficient; invalid actions (for example, close from open) can still enter transitioning and cause phantom optimistic updates.

Suggested fix
 import {
+  canApplyDisputeOperatorAction,
   disputeOperatorActionTransitions,
   type DisputeOperatorActionType,
   type DisputeRecord,
 } from '@quickwerk/domain';
@@
   const nextStatus = disputeOperatorActionTransitions[actionType];
-  if (!nextStatus || nextStatus === dispute.status) {
+  if (!nextStatus || !canApplyDisputeOperatorAction(dispute.status, actionType) || nextStatus === dispute.status) {
     return state;
   }

Also applies to: 48-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-web/src/features/disputes/dispute-queue-state.ts` around lines 1 -
5, The optimistic transition is being applied without verifying the action is
allowed from the current dispute status; before setting a dispute to
"transitioning" (the logic around disputeOperatorActionTransitions and the
optimistic update path that compares nextStatus !== dispute.status), validate
that the current dispute.status is included in
disputeOperatorActionTransitions[action].from (or the appropriate allowed-from
set) for the given DisputeOperatorActionType/DisputeRecord; if the action is not
allowed from the current status, abort the optimistic state change and surface
an error instead of flipping to transitioning. Ensure the check references
disputeOperatorActionTransitions and DisputeOperatorActionType so invalid
actions (e.g., close from open) cannot produce phantom optimistic updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts`:
- Around line 230-241: The check in postgres-dispute.repository.ts treats
resolvedAt as part of replay identity and can mark identical retries as
'transition-conflict'; update the logic in the block where current.status ===
input.nextStatus to ignore resolvedAt differences (or treat resolvedAt as
server-assigned) when deciding replayed vs conflict: compare resolutionNote
(normalize types, e.g., stringify/ISO when comparing current.resolved_at and
input.resolvedAt if you do compare), but return { ok: true, dispute:
mapDisputeRow(current), replayed: true } when the intent (nextStatus and
resolutionNote) matches, and only treat a true conflict when the action/status
or resolutionNote differ; reference variables: current.resolved_at,
input.resolvedAt, input.resolutionNote, input.nextStatus, and function
mapDisputeRow.

---

Duplicate comments:
In `@apps/admin-web/src/features/disputes/dispute-queue-state.ts`:
- Around line 1-5: The optimistic transition is being applied without verifying
the action is allowed from the current dispute status; before setting a dispute
to "transitioning" (the logic around disputeOperatorActionTransitions and the
optimistic update path that compares nextStatus !== dispute.status), validate
that the current dispute.status is included in
disputeOperatorActionTransitions[action].from (or the appropriate allowed-from
set) for the given DisputeOperatorActionType/DisputeRecord; if the action is not
allowed from the current status, abort the optimistic state change and surface
an error instead of flipping to transitioning. Ensure the check references
disputeOperatorActionTransitions and DisputeOperatorActionType so invalid
actions (e.g., close from open) cannot produce phantom optimistic updates.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7829bc5-d572-46c2-b0c8-2f5d5b93fb95

📥 Commits

Reviewing files that changed from the base of the PR and between ca9eb51 and 75c0acf.

📒 Files selected for processing (5)
  • apps/admin-web/next-env.d.ts
  • apps/admin-web/src/features/disputes/dispute-queue-state.ts
  • apps/admin-web/tsconfig.json
  • services/platform-api/src/disputes/disputes.controller.ts
  • services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/admin-web/next-env.d.ts
  • apps/admin-web/tsconfig.json

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts

Commit: 6852f3d5be853e5d9e542b00b2768b65dd9b630e

The changes have been pushed to the codexcoder/tec-25-operator-dispute-lifecycle branch.

Time taken: 2m 39s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts (1)

76-85: Extract the repeated dispute projection into a shared constant.

The same id::text, booking_id::text, ... resolution_note list is duplicated across seven queries. The next column change will be easy to miss in one path and break mapDisputeRow only there; a shared projection/helper would keep these reads in lockstep.

Also applies to: 102-111, 128-137, 150-159, 172-181, 208-217, 257-266

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts`:
- Around line 233-245: The in-memory repository's same-status path must match
Postgres behavior: in InMemoryDisputeRepository (method that handles status
transitions, e.g., the update/transition method currently returning a replay on
same-status) change the logic so when current.status === input.nextStatus you
normalize and compare resolution notes (treat undefined and null as equivalent)
and if they match return a replay ({ ok: true, dispute: ..., replayed: true }),
but if they differ return { ok: false, reason: 'transition-conflict',
currentStatus: current.status }; alternatively factor this compare/decision into
shared helper logic (e.g., a new function compareTransitionIntent) and call it
from both InMemoryDisputeRepository and PostgresDisputeRepository to avoid
duplication.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8fec5760-e0ae-42a5-9747-1c61b37e6596

📥 Commits

Reviewing files that changed from the base of the PR and between 75c0acf and 6852f3d.

📒 Files selected for processing (1)
  • services/platform-api/src/disputes/infrastructure/postgres-dispute.repository.ts

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.ts

Commit: d00f573f0427bb950c9c1dd545fc6032cf9d8732

The changes have been pushed to the codexcoder/tec-25-operator-dispute-lifecycle branch.

Time taken: 3m 10s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.ts`:
- Around line 16-18: The in-memory repository is returning references to the
internal DisputeRecord objects; update the repository methods (findById,
findByOrderId, listByAccountId, listByIds and any other listed-return methods)
to return defensive copies (deep clones) of records instead of the original
objects, and ensure save and transitionStatus store a clone of incoming records
rather than the caller's object. Use a safe deep-clone strategy supported by the
runtime (e.g., structuredClone if available, otherwise JSON deep-copy or a
utility) and apply it when reading from the internal Map (clone single records
returned by findById/findByOrderId, clone arrays returned by
listByAccountId/listByIds, etc.) and when putting/updating entries in
save/transitionStatus so repository state cannot be mutated by external callers.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b9f32d5-05cc-4d95-b28a-167deebd21be

📥 Commits

Reviewing files that changed from the base of the PR and between 6852f3d and d00f573.

📒 Files selected for processing (1)
  • services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.ts

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • services/platform-api/src/disputes/infrastructure/in-memory-dispute.repository.ts

Commit: aff91ffaed69cf7574c1e59d64c0a5fbcc871ff2

The changes have been pushed to the codexcoder/tec-25-operator-dispute-lifecycle branch.

Time taken: 3m 20s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@imKXNNY imKXNNY merged commit 68a517b into main Apr 12, 2026
1 check passed
@imKXNNY imKXNNY deleted the codexcoder/tec-25-operator-dispute-lifecycle branch April 13, 2026 07:55
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.

1 participant