# Issue 740: Approval Submit Conflict UX

## Context

Issue: https://github.com/pingcap-inc/NexusCRM/issues/740

Layout note: frontend path references in this issue design are implementation
anchors from the document's review window. Verify current frontend source
ownership against `frontend-l0-l1-ownership-inventory.md` before using them as
paths on `main`.

The approval submit service already raises `ErrRecordAlreadyPending` when a
record has an open pending approval instance. That error is a typed
`ConflictError`, but the public submit handler's legacy single-request error
path currently falls through to `400 INVALID_REQUEST`. The frontend approval
submit API client also passes the backend error message through to the modal,
which makes the user-facing toast expose implementation detail such as
`approval already exists with status='pending'`.

The endpoint is part of the Salesforce v63 REST process approvals surface:
https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_process_approvals.htm

## Current Flow

1. `ApprovalService.Submit` detects an existing pending instance and returns
   `ErrRecordAlreadyPending`.
2. `ApprovalHandler.HandleProcessApprovals` treats non-wrapper legacy submit
   requests as public HTTP errors through `respondApprovalSubmitError`.
3. `respondApprovalSubmitError` handles internal, permission, not-found, and
   validation errors, but not conflict errors, so conflict currently becomes
   `400 INVALID_REQUEST`.
4. `apiClient.post` preserves failed HTTP status and response body in
   `APIError`.
5. `approvalsAPI.submit` does not inspect `APIError.status`, so
   `SubmitApprovalModal` concatenates the raw message into the toast.

Wrapper requests with a `requests` array intentionally stay HTTP 200 with
per-item `ProcessResult.errors`, matching the existing bulk-capable handler
shape.

## Design

### Backend

Add an explicit `ConflictError` branch in `respondApprovalSubmitError` after
the internal-error guard and before the validation fallback:

- HTTP status: `409 Conflict`
- v63 error envelope: `apierr.SFErrorResponse(apierr.SFCodeConflict, err.Error())`

This keeps the single-submit HTTP error status aligned with the typed domain
error without changing the batch/wrapper `ProcessResult` contract.

Add a handler test beside the existing public submit error tests:

- service stub returns `automation.ErrRecordAlreadyPending`
- legacy single submit request returns HTTP 409
- error code is `CONFLICT`

### Frontend

Keep the modal catch block generic. Put submit-conflict translation in
`frontend/src/infrastructure/api/approvals.ts` so every submit caller benefits.

`approvalsAPI.submit` should catch only the submit HTTP call. If the caught
error is an `APIError` with status `409`, throw a new `Error` with a stable
message:

`This record is already pending approval. Refresh the record to see the latest approval status.`

All other errors are rethrown unchanged. This keeps generic API error handling
unchanged and avoids teaching the UI component about transport status codes.

Add a focused Vitest unit test for `approvalsAPI.submit`:

- mock `apiClient.post` to reject with `new APIError(..., 409)`
- assert `approvalsAPI.submit` rejects with the stable message
- mock a non-409 error and assert the original error is preserved

## Confidence Iterations

1. Contract pass: The backend change only maps a typed conflict to the existing
   Salesforce-shaped error array and does not change successful approval result
   shape.
2. Scope pass: No approval metadata, ProcessInstance model, step advancement,
   recall, reassign, or pending-state gate behavior changes.
3. Call-site pass: The modal already uses `getErrorMessage(err)`, so replacing
   the thrown message in the API client is sufficient and avoids duplicate UI
   logic.
4. Regression pass: Existing wrapper request behavior remains HTTP 200 with
   per-item errors; only the legacy single-submit HTTP error path changes.
5. Verification pass: The change can be proven with one backend handler test,
   one frontend API-client unit test file, targeted backend/frontend tests, and
   `git diff --check`.

No unresolved design risks remain after these passes.
