# Issue 797: Recall Action Failure Surfacing

Issue: https://github.com/pingcap-inc/NexusCRM/issues/797
Parent issue: https://github.com/pingcap-inc/NexusCRM/issues/777
Related durable replay issue: https://github.com/pingcap-inc/NexusCRM/issues/798

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`.

## Problem

Approval action side effects can fail even when the core approval transition
should still succeed. The backend service already has a generic
`ProcessActionFailure` shape and can attach failures to the service response
under `ActionFailures`, but the public process-approvals response and frontend
approval client do not carry those non-fatal failures to UI callers.

For Recall, that means the user can see a clean success message even when a
best-effort Recall action failed. The correct UX is still success for the core
recall, plus an explicit warning that one or more configured actions did not
complete.

After rebase onto current `main`, Recall FieldUpdate execution is owned by the
durable replay/outbox path from #798/#866. This PR therefore focuses on the
remaining transport and display gap: when the approval service returns
user-safe, synchronous `ActionFailures`, the REST process-approvals response and
record-detail UI preserve and display them without changing the core approval
success contract.

The endpoint is 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`, `processAction`, and `Recall` can attach
   user-safe `ProcessActionFailure` entries to their returned `models.SObject`
   as `ActionFailures` for synchronous, non-fatal action-warning paths.
2. `ApprovalHandler.HandleProcessApprovals` calls `buildProcessResult`, which
   maps standard `ProcessResult` fields but drops `ActionFailures`.
3. `frontend/src/features/approvals/approvalTypes.ts` has no
   `ProcessActionFailure` type and `ProcessResult` has no optional warning
   field.
4. `approvalsAPI._unwrapProcessResult` returns the first successful result, but
   callers discard it in approval action flows.
5. `ApprovalBanner` calls `approvalsAPI.recall`, shows success, and refreshes the
   page without rendering any non-fatal action warning.

## Design

### Recall Execution Boundary

Do not reintroduce in-request best-effort Recall record mutations in this PR.
Current `main` owns the safe engine contract:

- Recall's authoritative state transition stays inside the existing transaction:
  validate the pending `ProcessInstance`, write `ProcessInstance.Status =
  Removed`, mark pending runtime nodes `Removed`, and delete pending work items.
- The Recall `ProcessAction` batch is resolved inside that transaction.
  Object-resolution and action-query failures fail closed and roll back the
  recall because the engine cannot safely commit `Removed` state without knowing
  which configured record mutations exist.
- Recall `FieldUpdate` targets are frozen and enqueued to the durable replay
  outbox in the same transaction as the recall state transition. Enqueue or
  target-freezing failures abort the transaction.
- Recall `FieldUpdate` execution failures happen later in the replay worker and
  are retried or escalated through durable replay observability. They are not
  synchronous `actionFailures` on the Recall REST success response.
- Post-commit notification paths remain best effort. User-safe failures from
  those synchronous paths may be returned as `ActionFailures`.

The important review guardrail is that UI warning needs must not punch a new
hole in the engine contract. If product needs end-user visibility for eventual
durable replay exhaustion, that should be added as a replay-status/read-model
surface and then displayed through the same record-detail warning area.

### Backend Bridge

Add an optional `ActionFailures []automation.ProcessActionFailure` field to the
REST `ProcessResult` DTO with JSON name `actionFailures,omitempty`. Populate it
in `buildProcessResult` when the service response contains `ActionFailures`.

This is an explicit Nexus extension to the v63 `ProcessResult` body, not a
replacement for any Salesforce-modeled field. The canonical v63 fields
(`actorIds`, `entityId`, `errors`, `instanceId`, `instanceStatus`,
`newWorkitemIds`, `success`) remain unchanged, and the extension is omitted when
there are no warnings. The central API contract carve-out is recorded in
`docs/design/salesforce-v63-api-contract.md`.

Accepted input shapes from the service response:

- `[]automation.ProcessActionFailure`, the native service type.
- `[]interface{}` containing `automation.ProcessActionFailure` or
  `map[string]interface{}` entries, so the bridge is robust to SObject-like test
  payloads.

Add a focused handler test proving a successful process result can include
`actionFailures` while `success` remains true.

### Frontend Types And API

Add `ProcessActionFailure` to
`frontend/src/features/approvals/approvalTypes.ts` with the same field names as
the backend JSON payload:

- `actionName`
- `actionType`
- optional `targetObject`, `targetId`, `field`
- `reason`

Add `actionFailures?: ProcessActionFailure[]` to `ProcessResult`.

Keep `_unwrapProcessResult` success semantics unchanged: `success=false` still
throws, and `success=true` returns the full result including
`actionFailures`. Add a frontend API unit test that mocks a successful response
with `actionFailures` and asserts the returned result preserves it.

### Recall UI

Change `ApprovalBanner`, `ApprovalHistory`, and `SubmitApprovalModal` so
approval completions pass the returned `ProcessResult`, when one exists, to the
record-detail completion callback. `MetadataRecordDetail` stores
`result.actionFailures ?? []` before refreshing the approval status and renders
the amber warning banner outside the pending-only banner. This keeps the
warning visible after a successful Recall refresh clears `pendingStatus` and
unmounts `ApprovalBanner`, while later approval completions without warnings
clear stale Recall warnings.

The warning banner should:

- use a familiar warning icon from `lucide-react`
- say the approval action completed but some configured actions failed
- include concise per-action detail using `actionName` or `actionType`, plus the
  failure reason
- clear when the next approval action completes without warnings or the record
  detail page is recreated for another record

Do not turn these warnings into `errors`; doing so would undo the service
contract that a core approval operation can succeed while a non-fatal configured
action warning is displayed.

Add component tests that assert:

- `approvalsAPI.recall` resolves with a successful result containing
  `actionFailures` and `ApprovalBanner` passes that result to the parent
- success toast is still called
- `MetadataRecordDetail` keeps the warning visible after the completion callback
  clears the mocked pending approval state
- `SubmitApprovalModal` and `ApprovalHistory` completions clear a prior warning
  when their successful `ProcessResult` contains no `actionFailures`

## Boundaries

- No approval business policy moves into the frontend.
- No object-specific, profile-specific, record-type-specific, tenant-specific,
  or pack-specific branches are introduced.
- No new source of truth for ProcessAction execution is added; the UI only
  displays the service-owned warning payload.
- The optional REST extension is limited to non-fatal warning visibility and is
  omitted for ordinary v63-compatible success responses.
- The PR does not modify durable replay, retry policy, operator escalation, or
  replay failure read models; those remain owned by the #798/#866 path and any
  follow-up UX contract for eventual replay failures.

## Iteration Log

1. Initial frontend-only reading was insufficient. Source inspection showed the
   service response can contain `ActionFailures`, but the REST
   `buildProcessResult` bridge currently drops it, leaving the frontend with
   nothing to display. The design therefore includes a minimal backend bridge.
2. Treating `actionFailures` as thrown errors was rejected because the core
   Recall result must remain successful when only best-effort actions fail.
3. A warning toast alone would survive the record refresh, but the issue asks
   approval UI call sites to render a warning banner. A local banner inside
   `ApprovalBanner` was rejected after lifecycle review because a successful
   refresh clears `pendingStatus` and unmounts that component. The final design
   lifts the warning to `MetadataRecordDetail`.
4. The backend field is an append-only optional Nexus extension. This preserves
   all documented v63 `ProcessResult` fields and keeps existing clients
   unaffected when no warnings exist.
5. Review of the first implementation found a restart/cancellation-safety
   regression when Recall FieldUpdates ran post-commit.
6. Review of the second implementation found that the in-transaction
   best-effort path still risked committing approval state while swallowing
   infrastructure/cancellation failures.
7. Rebase onto current `main` brought in the durable replay solution from
   #798/#866. The conflict resolution keeps that engine model and narrows this
   PR to the REST/UI propagation layer for synchronous, user-safe
   `ActionFailures`.

## Confidence Checklist

- Recall FieldUpdates are frozen and enqueued durably inside the core Recall
  transaction; execution failures are not converted into synchronous REST
  `actionFailures`.
- Recall action object-resolution, action-query, target-freezing, and replay
  enqueue failures roll back, because the engine cannot safely know which
  configured record mutations should run.
- `_unwrapProcessResult` already returns the whole successful result, so the API
  change is type/test coverage rather than a behavior rewrite.
- The Recall call site is the only current approval UI caller that invokes
  `recall`; the parent warning state preserves the warning across approval
  refresh and pending-banner unmount.
- Success, error, and refresh behavior remain separate: failed `success=false`
  results throw; successful warnings render; `onActionComplete` still refreshes.
- Focused REST handler, frontend API, banner, and record-detail lifecycle tests
  cover the path from a service-owned `ActionFailures` payload to a visible
  warning.

Conflict-resolution risk is closed. The remaining product question is whether
eventual durable replay exhaustion should become a user-facing record warning;
that needs a durable replay status surface rather than another in-request Recall
mutation path.
