# Issue 737: Approval Submit State Sync

Issue: https://github.com/pingcap-inc/NexusCRM/issues/737
Status: design locked before implementation
Date: 2026-05-14

## Purpose

Issue #737 covers two approval-engine defects that can split the user's view of
approval state from the record's real workflow state:

- the backend can create or advance approval runtime rows while silently
  skipping configured `ProcessAction` `FieldUpdate` mutations when the action
  query fails
- the record detail UI can re-enable Submit for Approval when approval-chain
  assembly is incomplete, even though a pending `ProcessInstance` exists

The fix must keep the generic approval substrate generic. It must not encode
application object names, status-field names, pack-specific status values, or
tenant policy. The only facts this design uses are platform approval runtime
facts: `ProcessInstance`, `ProcessAction`, lifecycle event names, and metadata
stored on those records.

## Runtime Contract

`ProcessInstance.Status` is the authoritative record-level approval state for
the record detail gate. Approval-chain assembly is a richer display path, not
the source of truth for whether the record is pending approval.

For record detail:

- If the latest `ProcessInstance` for a record has `Status='Pending'`, the
  Submit for Approval action is disabled and the pending banner renders.
- That pending gate holds even when `ProcessNode`,
  `ProcessInstanceNode`, or `ProcessInstanceWorkitem` sub-queries fail.
- Chain display should degrade independently. Missing node or work-item data
  may produce a shorter chain or omit approver details, but it must not erase
  the pending record-level state.
- Degraded pending banners without a `ProcessInstanceWorkitem` id are
  non-actionable. They may gate Submit and show pending state, but they must
  not render approve, reject, reassign, or recall controls.
- If there is no latest `ProcessInstance`, the UI has no approval history and
  Submit remains controlled by the existing runtime submit endpoint.

For approval mutations:

- Lifecycle `ProcessAction` object-resolution or fetch failure is a
  transaction error whenever the lifecycle event can own `FieldUpdate`
  mutations inside that transaction.
- No `ProcessInstance`, node, work-item, recall, reassign, approve, reject, or
  finalization state may commit when the engine cannot determine which
  configured actions must run for that lifecycle event.
- A missing or unresolvable `ProcessDefinition.TableEnumOrId` is treated the
  same as an action query failure on required lifecycle paths; it is an
  internal infrastructure/metadata failure, not a valid no-actions result.
- An empty successful `ProcessAction` query remains valid and commits normally.
- `FieldUpdate` execution failures already abort the transaction and keep that
  behavior.
- Current-step approval/rejection `FieldUpdate` actions execute before
  downstream step criteria and approver resolution so transition selection sees
  the same in-transaction target-record state that will commit.
- Notification actions stay best-effort after commit. A successful action
  fetch may contain alerts whose delivery later fails without rolling back the
  approval state.

This changes fetch errors from "warn and skip" to "warn and abort" for the
approval lifecycle mutation sites that currently execute `FieldUpdate` actions
inside the same transaction: `Submit`, `FinalApproval`, step approval/rejection,
next-step approval, `Recall`, and `Reassign`.

## Implementation Shape

Backend:

- Add small approval-service helpers that wrap object resolution and
  `fetchProcessActions`, returning contextual internal errors when required
  lifecycle action resolution fails.
- Replace every transactional object-resolution or `fetchProcessActions`
  warn-and-continue block with the helpers and return errors from the
  transaction.
- Preserve the existing no-actions path by treating `nil` or empty successful
  results as no-op.
- Leave post-commit notification execution unchanged.

Frontend:

- Extend `ApprovalChainResult` with the latest process instance status and
  submitter id.
- Have `buildApprovalChain` propagate `ProcessInstance.Status` into the result.
- In Nexus backend mode, fetch the latest process instance summary first and
  keep that summary in the result even if downstream chain assembly fails.
- Derive the hook's `status` and pending banner model from the authoritative
  `processInstanceStatus` before falling back to chain inference.
- Keep `pendingStatus` specific to `Pending`; it is the banner and action-gate
  payload, not a generic completed-history marker.

## Self-Review Iterations

Iteration 1 checked the issue text against the current code. The original
failure is present: `Submit`, `FinalApproval`, step lifecycle events, and
`Recall` all log `fetchProcessActions` failures and keep the transaction alive.
`Reassign` has the same transactional FieldUpdate shape, so the same invariant
applies there too.

Iteration 2 checked the frontend failure against the current data flow.
`getFullApprovalChain` already has a backend summary endpoint, but
`useRecordApproval` discards that authoritative status and infers pending state
from the display chain. A pending instance with only a submitted step, or with
failed sub-query data, can therefore produce `pendingStatus=null`.

Iteration 3 checked the metadata boundary. The design does not add CRM object
branches, tenant fields, hardcoded status-field values, or pack payload. It
only tightens generic approval lifecycle semantics.

Iteration 4 checked rollback and degradation. Backend query failure aborts the
transaction before returning success; frontend query degradation keeps the
pending gate while allowing chain details to be partial.

## Verification

Focused backend coverage:

- Submit succeeds when the `ProcessAction` query succeeds with no actions.
- Submit returns an error and the transaction rolls back when the Submit-event
  `ProcessAction` query fails.
- Final approval, step-level lifecycle events, recall, and reassign action-fetch
  sites all return their fetch error from the transaction instead of committing
  partial state.
- Recall and Reassign return internal errors and roll back their runtime
  mutations when required process-object resolution fails or resolves empty.

Focused frontend coverage:

- A pending `ApprovalChainResult` with no assembled chain still produces
  `status='Pending'` and a non-null pending banner model.
- Chain-only inference continues to produce pending state for legacy paths that
  do not provide `processInstanceStatus`.
- Completed process statuses do not create a pending banner model.
- Pending banners with no work item id do not render approval action controls.

Baseline verification:

- `cd backend && go test ./internal/features/automation`
- `cd frontend && npm run test -- useRecordApproval approvals.test`
- `git diff --check`

Full repository gates remain the standard:

- `cd backend && go test ./...`
- `cd shared && go test ./...`
- `cd frontend && npm run build`

If `npm run build` rewrites `frontend/public/version.json`, that generated
change is not part of this issue and should be reverted before commit.
