# Issue 841 Record Detail Approver Actions

Status: implementation-ready
Scope class: approval runtime UI contract
Date: 2026-05-15

## Purpose

Issue #841 tracks a first-load record detail gap. The approval queue can see a
current user's pending `ProcessInstanceWorkitem`, but the submitted record
detail page can render only the pending banner and not the Approve, Reject, or
Reassign controls for that same approver.

The fix must stay generic approval substrate. It must not branch on object API
names, approval process developer names, record types, profiles, or tenant
metadata.

## Current Failure

The two UI surfaces use different pending-work-item sources:

- `/approvals` calls the backend-only
  `/services/data/v63.0/process/approvals/pending` projection. That path reads
  `ProcessInstanceWorkitem` rows in system context, filters them to the current
  user, and calls the existing Approve, Reject, and Reassign APIs with the work
  item ID.
- Record detail calls
  `/services/data/v63.0/process/approvals/records/:recordId/instance`, then
  separately tries to build its chain from public SOQL reads of
  `ProcessNode`, `ProcessInstanceNode`, and `ProcessInstanceWorkitem`.
- In Nexus backend mode, the record-specific summary endpoint does not include
  pending work items. If the public `ProcessInstanceWorkitem` read is empty or
  unavailable, `buildApprovalChain` has no current-user assignment to attach to
  the pending step.
- `ApprovalBanner` already renders Approve, Reject, and Reassign when
  `RecordApprovalStatus.pendingWorkItems` contains an assignment whose
  `actorId` matches the runtime user. With no assignment, it correctly fails
  closed and shows only the pending state.

The failure is therefore a data-contract mismatch, not a missing button
component or a missing action executor.

## Contract

- `ProcessInstanceWorkitem` remains the source of truth for approver action
  eligibility.
- Record detail must receive the pending work items for the latest pending
  process instance through the same backend-owned approval runtime read path as
  the queue.
- The record-detail chain builder remains object-neutral. It receives
  work-item assignments and chooses controls by current-user work item ID.
- Approve and reject continue to call
  `POST /services/data/v63.0/process/approvals` with Salesforce wire verbs
  `Approve` and `Reject`.
- Reassign continues to call the same endpoint with the Salesforce wire verb
  `Reassign` and `nextApproverIds`.
- Users without a pending work item for the current instance do not see
  approver-only controls.
- The backend summary may expose all pending assignments for the latest process
  instance so the detail page can show "Waiting for" names and so any eligible
  approver can resolve their own work item. This mirrors the existing intended
  SOQL-mode data model and does not introduce tenant-specific policy.
- Recall visibility is out of scope for this issue.

## Implementation Shape

Backend:

- Add a backend-only pending-work-item summary type to
  `backend/internal/features/automation/approval_service.go`.
- Extend `ProcessInstanceSummary` with
  `PendingWorkItems []ProcessInstancePendingWorkItemSummary`.
- In `GetLatestProcessInstance`, after loading the newest process instance,
  read pending `ProcessInstanceWorkitem` rows for that instance when
  `ProcessInstance.Status == Pending`.
- Resolve actor display names from `User` records by ID as best-effort
  enrichment. Missing user rows should not hide action controls because actor
  IDs and work-item IDs are sufficient.
- Return an error if the pending work-item query itself fails for a pending
  instance. Silently dropping that data would reproduce the issue by hiding
  valid controls.

Frontend:

- Extend `APIProcessInstanceSummary` with optional `pendingWorkItems`.
- Map backend summary work items into the existing `PendingWorkItemData` shape
  consumed by `buildApprovalChain`.
- In Nexus backend mode, use `inst.pendingWorkItems` as the preferred pending
  assignment source instead of querying `ProcessInstanceWorkitem` through the
  public SOQL API.
- If the dependent `ProcessNode` or `ProcessInstanceNode` reads are empty or
  unavailable, keep a minimal pending chain step from the summary work-item
  assignments so the banner still resolves first-load approver actions.
- Keep Salesforce/SOQL mode unchanged.
- Keep the existing `ApprovalBanner` action execution path unchanged. The
  banner will render Approve, Reject, and Reassign once the current user's
  assignment is present.

Tests:

- Backend unit coverage proves `GetLatestProcessInstance` includes pending
  work items for the latest pending instance and keeps IDs even when display
  names are best effort.
- Frontend API coverage proves Nexus backend mode builds pending chain
  assignments from the summary payload and does not require a public
  `ProcessInstanceWorkitem` query.
- Chain-builder coverage proves summary-only pending assignments still produce
  a pending step with a work-item ID.
- Existing banner/helper coverage continues to prove the current user's work
  item is selected before rendering or executing approver actions.

## Edge Cases

- Terminal latest instance: no pending work items are returned, and record
  detail renders history without approver controls.
- Pending instance with no work items: return an empty list; the banner shows
  pending status but no approver-only controls.
- Multiple approvers: return every pending work item. The frontend finds the
  current user's assignment and preserves the compact multi-approver display.
- Reassigned work item: the next refresh reads the new `ActorId` from the
  summary and the old approver loses controls.
- User-name lookup failure: keep `actorId` and `workItemId`; action visibility
  still works.
- Public SOQL visibility differences: irrelevant in Nexus backend mode because
  pending assignments now arrive through the backend approval runtime summary.
- Missing process/node details: retain a generic pending step from summary
  assignments rather than hiding approver actions.

## Rejected Alternatives

1. Put Approve, Reject, and Reassign in `_System_StandardAction`.
   Issue #753 resolved that approval verbs are Salesforce v63 wire-contract
   verbs, not metadata-dispatched standard actions.
2. Duplicate the approval queue query on the frontend and filter by record ID.
   This would overfetch the user's whole queue and still would not help
   multi-approver display for other pending assignments.
3. Continue relying on public SOQL reads of `ProcessInstanceWorkitem` in Nexus
   backend mode. That is the observed weak point: the queue has the work item,
   while record detail can fail closed without it.
4. Render controls from `ProcessInstance.Status == Pending` alone. That would
   expose approver actions to users who do not own a pending work item and would
   violate backend authorization semantics.

## Verification

- Focused backend:
  - `cd backend && go test ./internal/features/automation`
- Focused frontend:
  - `cd frontend && npm run test -- approvals useRecordApproval ApprovalBanner`
- Baseline:
  - `cd backend && go test ./...`
  - `cd shared && go test ./...`
  - `cd frontend && npm run build`
  - `git diff --check`

`npm run build` rewrites `frontend/public/version.json`; revert that generated
change unless it is intentionally in scope.

## Design Iteration Notes

1. Initial inspection found `ApprovalBanner` already implements Approve,
   Reject, and Reassign, and it already uses the same `approvalsAPI` execution
   methods as the queue. That ruled out a UI-only button patch.
2. The next option was to make the frontend call `/approvals/pending` from
   record detail. That would work for the current user only, but it would
   duplicate queue behavior and drop the process-instance-centric source of
   truth that the chain builder needs for multi-approver display.
3. The selected design extends the existing record-specific backend summary.
   That keeps the resolver process-instance driven, avoids object branches, and
   lets record detail use the same pending-work-item truth as backend
   authorization.
4. Source review found the dependent node reads already fail closed to empty
   arrays. The implementation therefore adds a minimal summary-only pending
   chain fallback so those existing catch paths cannot hide valid first-load
   approver actions.
5. Final confidence check: action visibility depends on
   `findPendingAssignmentForUser(pendingAssignments, currentUserId)`;
   `pendingAssignments` comes from `ProcessInstanceWorkitem`; and the action
   calls already execute against that work-item ID. Supplying those assignments
   from the backend summary closes the issue without changing approval
   semantics.

References:

- Issue #841: https://github.com/pingcap-inc/NexusCRM/issues/841
- Standard-action decision: https://github.com/pingcap-inc/NexusCRM/issues/753
- Multi-approver prior design: docs/design/issue-748-multi-approver-record-detail.md
