# Remediation Phase Metadata and State Machine Advancement

## Table of Contents

- [Remediation Phase Metadata and State Machine Advancement](#remediation-phase-metadata-and-state-machine-advancement)
  - [Table of Contents](#table-of-contents)
  - [Introduction](#introduction)
  - [Motivation or Background](#motivation-or-background)
  - [Current Problems](#current-problems)
  - [Goals](#goals)
  - [Non-Goals](#non-goals)
  - [Detailed Design](#detailed-design)
    - [Design Principles](#design-principles)
    - [Phase Metadata](#phase-metadata)
    - [Approval Flow Projection](#approval-flow-projection)
    - [State Machine Events](#state-machine-events)
    - [Automatic Advancement](#automatic-advancement)
    - [API Behavior](#api-behavior)
    - [Compatibility Strategy](#compatibility-strategy)
    - [Suggested Delivery Order](#suggested-delivery-order)
  - [Test Design](#test-design)
    - [Functional Tests](#functional-tests)
    - [Scenario Tests](#scenario-tests)
    - [Compatibility Tests](#compatibility-tests)
  - [Impacts and Risks](#impacts-and-risks)
  - [Unresolved Questions](#unresolved-questions)

## Introduction

This document proposes adding explicit remediation phase metadata and moving workflow advancement decisions into the backend state machine. The goal is to remove frontend knowledge such as "approve, then fetch the latest ticket, then submit" while keeping the current remediation ticket data model and public API mostly compatible during migration.

This design is a follow-up to:

- `docs/design/2026-04-15-remediation-approval-optimization.md`
- `docs/design/2026-04-20-remediation-workflow-definition-config-plan.md`

Those documents define the approval-flow view and workflow definition direction. This document focuses on three additional pieces:

1. Backend-provided phase metadata, especially whether a phase requires user input.
2. Backend-owned state machine advancement after approval events.
3. A clearer `ticket.phase` semantic: the phase should represent the workflow's current node, including while that node is waiting for approval.

## Motivation or Background

The current remediation approval flow works, but the frontend has to understand backend implementation details to provide a reasonable user experience.

For a non-final phase, the backend flow is currently:

1. `POST /remediationtickets/{ticket_id}/approve` finishes the current approval task.
2. The backend syncs the ticket into the approved target phase with status `pending`.
3. `POST /remediationtickets/{ticket_id}/submit` validates and stores the target phase input, then creates the next approval request.

The two-step model is useful when the user who submits phase data and the user who approves that data are different people. In that model, `submit` means "I have completed this phase's data and want to request approval", while `approve` means "I approve the submitted data".

In the current remediation UX, several approval nodes expect the approver to provide the next phase's input as part of the approval decision. For those nodes, forcing a separate visible submit step is confusing even when the phase does require input. It is even more confusing for phases whose `ValidateAndUpdateTicket` is a no-op. For example:

- `rdg_final_decision_maker`
- `commercial_operation`
- `regional_gm`

These phases may require approval checks, but they do not collect additional ticket fields. The user naturally expects "Approve" to be enough. Today, however, the workflow still requires `submit` because "advance to the next approval" is coupled with "submit phase input".

The immediate frontend workaround can hide this by calling `submit` in the background. That improves UX, but it leaks backend state machine rules into the frontend and creates avoidable risks around versions, multi-approver approvals, and future phase changes. A real one-step UX should be represented by one backend event.

## Current Problems

### Frontend infers phase behavior

The frontend currently infers whether a phase needs input from local phase definitions. This is not authoritative because backend phase validation and backend workflow transitions are the real source of truth.

### Submit has two meanings

`SubmitPhaseTransitionApproval` currently means both:

- persist current phase input
- advance the workflow and create the next approval

For phases without input, only the second meaning applies, but the frontend still has to call a submit endpoint.

### Approval does not own post-approval advancement

`ApproveTicketPhase` finishes the approval and syncs the ticket into the next phase, but it does not decide whether the new phase can be automatically advanced. That decision is pushed to the client.

### Approve and input are split even when they are one user action

For phases that require input, the frontend currently has to call `approve`, refetch the ticket, then call `submit` with the input. This means an approval can succeed while the input submission fails, leaving the ticket in an unexpected intermediate state. The backend should instead accept target-phase input on the approval request when the approved target phase requires input.

### Ticket phase does not consistently mean current workflow node

When a ticket is `approving`, the persisted `ticket.phase` can still represent the previously submitted phase. The actual node waiting for approval must be inferred from `ticket.appr_id` and the phase-approval record's `to_phase`. This is hard for clients and operators to reason about. `ticket.phase` should consistently represent the current workflow node:

- `pending` or `rejected`: the current node is waiting for input or correction.
- `approving`: the current node is waiting for approval.
- final statuses: the current node is the final phase.

### Flow projection lacks action hints

`GetApprovalFlow` returns ordered nodes and statuses, but it does not explicitly tell the frontend what action is expected for the current user, such as:

- approve only
- approve and provide input
- provide input
- wait for other approvers

## Goals

1. Add backend-owned metadata that describes whether a phase requires input.
2. Include the metadata in approval-flow projection so the frontend can render actions without local business inference.
3. Move workflow advancement decisions into backend state machine event handling.
4. Keep existing endpoint compatibility while introducing clearer semantics.
5. Support no-input phases by automatically advancing after approval when safe.
6. Support a true one-step approve-with-input backend event for phases that require input.
7. Make `ticket.phase` consistently represent the current workflow node.

## Non-Goals

1. Do not replace the approval service.
2. Do not redesign remediation into a generic workflow engine in this iteration.
3. Do not rename persisted phase enum values.
4. Do not change database schema unless implementation proves that metadata cannot be derived from code/config.
5. Do not remove `submit` immediately; keep it for compatibility with older clients and workflows where input submission is intentionally separate from approval.

## Detailed Design

### Design Principles

1. Backend owns workflow truth and phase behavior.
2. Frontend renders the current action; it should not orchestrate internal workflow transitions.
3. Phase input and workflow advancement should be separate concepts.
4. No-input phases should not require a visible submit action.
5. Automatic advancement must be idempotent and guarded by ticket status, phase, approval status, and version consistency.
6. If an approval target phase requires input, the approval event should validate and submit that input before the workflow advances further.
7. `ticket.phase + ticket.status` should be enough to understand the current workflow position.

### Phase Metadata

Introduce phase metadata in the backend workflow definition.

Suggested shape:

```go
type RemediationPhaseMetadata struct {
    Phase          entity.RemediationTicketPhase `json:"phase"`
    Label          string                        `json:"label"`
    Optional       bool                          `json:"optional"`
    RequiresInput  bool                          `json:"requires_input"`
    InputFields    []RemediationPhaseInputField  `json:"input_fields,omitempty"`
    AutoAdvance    bool                          `json:"auto_advance"`
}

type RemediationPhaseInputField struct {
    Name     string `json:"name"`
    Label    string `json:"label"`
    Required bool   `json:"required"`
    Type     string `json:"type"`
}
```

The minimal first version only needs:

- `requires_input`
- `auto_advance`

`input_fields` can be added later if the frontend should fully generate forms from backend metadata. Until then, frontend can keep rendering local form controls, but it should use backend `requires_input` to decide whether an input step exists.

The source of `requires_input` and `auto_advance` should be the phase handler, not an independent workflow table. Each phase can expose default methods such as `RequiresInput()` and `AutoAdvance()`. The workflow definition layer should keep owning phase order, labels, optional flags, and config overrides, then merge in handler-provided behavior metadata. This keeps `requires_input` close to `ValidateAndUpdateTicket` and reduces drift between metadata and actual validation.

Recommended initial metadata:

| Phase | Requires input | Auto advance after approval |
| --- | --- | --- |
| `created` | false | false |
| `region_tse_leader` | true | false |
| `rdg_sla_owner` | true | false |
| `global_deal_desk` | true | false |
| `rdg_final_decision_maker` | false | true |
| `commercial_operation` | false | true |
| `regional_gm` | false | true |
| `succeed` | false | false |
| `closed` | false | false |

`created` is a special case because ticket creation initializes the ticket and immediately sends the first approval. It should not be treated as a normal auto-advance phase in this design.

### Approval Flow Projection

Extend `entity.RemediationApprovalFlowNode` with metadata and action hints.

Suggested fields:

```go
type RemediationApprovalFlowNode struct {
    Phase         RemediationTicketPhase               `json:"phase"`
    Label         string                               `json:"label"`
    Status        RemediationApprovalFlowNodeStatus    `json:"status"`
    Current       bool                                 `json:"current"`
    Conditional   bool                                 `json:"conditional"`
    Approvers     []RemediationApprovalFlowApprover    `json:"approvers"`
    ApprovalID    uint64                               `json:"approval_id"`
    Message       string                               `json:"message"`

    RequiresInput bool                                 `json:"requires_input"`
    AutoAdvance   bool                                 `json:"auto_advance"`
    Action         RemediationApprovalFlowNodeAction    `json:"action"`
}
```

Suggested action enum:

```go
type RemediationApprovalFlowNodeAction string

const (
    RemediationApprovalFlowNodeActionNone              RemediationApprovalFlowNodeAction = "none"
    RemediationApprovalFlowNodeActionApprove           RemediationApprovalFlowNodeAction = "approve"
    RemediationApprovalFlowNodeActionApproveWithInput  RemediationApprovalFlowNodeAction = "approve_with_input"
    RemediationApprovalFlowNodeActionSubmitInput       RemediationApprovalFlowNodeAction = "submit_input"
    RemediationApprovalFlowNodeActionWait              RemediationApprovalFlowNodeAction = "wait"
)
```

The frontend can then render:

- `approve_with_input`: show approval checks and input fields, primary button "Approve and submit"
- `approve`: show approval checks, primary button "Approve"
- `submit_input`: show input fields, primary button "Submit"
- `wait`: show who owns the current step
- `none`: no action

### Current Ticket Phase Semantics

`ticket.phase` should be updated as soon as the workflow enters a node, not only after that node is approved.

For example, when the `rdg_final_decision_maker` phase creates an approval for `global_deal_desk`, the ticket should become:

```text
phase  = global_deal_desk
status = approving
appr_id = <global deal desk approval id>
```

After that approval is completed:

- if `global_deal_desk.requires_input == true`, the ticket temporarily becomes `phase = global_deal_desk`, `status = pending` while backend approval handling submits the provided input.
- after input is accepted, backend creates the next approval and sets `phase` to that next approval node.

This keeps the raw ticket, approval flow projection, and user-visible workflow aligned.

### State Machine Events

Introduce explicit workflow events inside the remediation service. This can start as internal methods; it does not need to expose new public APIs immediately.

Suggested events:

```go
type RemediationWorkflowEvent string

const (
    RemediationWorkflowEventSubmitInput RemediationWorkflowEvent = "submit_input"
    RemediationWorkflowEventApprove     RemediationWorkflowEvent = "approve"
    RemediationWorkflowEventReject      RemediationWorkflowEvent = "reject"
    RemediationWorkflowEventCancel      RemediationWorkflowEvent = "cancel"
)
```

The current public endpoints map to events:

- `SubmitTicketPhase` -> `submit_input`
- `ApproveTicketPhase` -> `approve`
- `RejectTicketPhase` -> `reject`
- `CancelTicket` -> `cancel`

The important change is that event handling owns any follow-up advancement. The frontend should not chain endpoint calls to advance workflow internals.

`ApproveTicketPhase` should accept optional target-phase input. If the approved target phase has `requires_input == true`, the backend must validate that input before finishing the approval. Invalid or missing input should fail the approve request and should not finish the approval task. After the approval is finished and the ticket is synced into the target phase, the backend submits the already-validated input and continues the workflow.

### Automatic Advancement

After an approval event, the backend should:

1. Finish the approval task.
2. Sync the approval result into the remediation ticket.
3. If the ticket moved to a non-final phase:
   - load the phase metadata
   - if `requires_input == false` and `auto_advance == true`, advance internally
   - stop when reaching a phase that requires input, is waiting for approval, is final, or is closed

Pseudo-code:

```go
func (s *svc) ApproveTicketPhase(ctx context.Context, id uint64, param ApprovePhaseParam) error {
    if err := s.validateApprovePhaseInput(ctx, id, param); err != nil {
        return err
    }

    if err := s.finishApprovalTask(ctx, id, param); err != nil {
        return err
    }

    if err := s.syncTicket(ctx, id); err != nil {
        return err
    }

    if err := s.submitApprovedPhaseInput(ctx, id, param); err != nil {
        return err
    }

    return s.advanceNoInputPhases(ctx, id)
}

func (s *svc) advanceNoInputPhases(ctx context.Context, ticketID uint64) error {
    for {
        ticket, err := s.repo.LoadTicket(ctx, ticketID)
        if err != nil {
            return err
        }

        if ticket.Status != entity.RemediationTicketStatusPending {
            return nil
        }
        if ticket.Phase == nil {
            return fmt.Errorf("ticket phase is nil")
        }

        meta := s.workflow.Metadata(*ticket.Phase)
        if meta.RequiresInput || !meta.AutoAdvance {
            return nil
        }

        actor := actors.NewRemediationTicketActor(ctx, s.insComps(ctx), ticket)
        if err := actor.SendApproval(rtsvcapi.TransferTicketParam{
            Version:        ticket.Version,
            SubmitterEmail: ticket.CurrentResponsible,
        }); err != nil {
            return err
        }
    }
}
```

The loop is intentional but must be bounded by workflow length in implementation to avoid infinite loops caused by a bad workflow definition.

The key guardrails:

- only run when the ticket is `pending`
- only run when the current phase metadata says no input is required
- use latest ticket version
- use current responsible person as the internal submitter
- stop at any phase that requires input
- stop at final phases
- stop when a new approval request is created and ticket status becomes `approving`

### API Behavior

#### Existing approve endpoint

Keep:

- `POST /api/v1/devops/remediationtickets/{ticket_id}/approve`

New behavior:

- The request body accepts the same phase input fields as `SubmitTicketPhase`.
- If the approved target phase requires input and approve input is missing or invalid, the approve request fails before the approval task is finished.
- If the approved target phase requires input and approve input is valid, the backend finishes the approval, syncs the ticket, submits the target-phase input, and creates the next approval or reaches final state.
- If the approved target phase does not require input and is auto-advanceable, backend advances through it and creates the next approval request or reaches final state.
- If the approved target phase does not require input but is not auto-advanceable, backend stops at that phase for explicit operational handling.

#### Existing submit endpoint

Keep:

- `POST /api/v1/devops/remediationtickets/{ticket_id}/submit`

Behavior stays valid for:

- phases that require input
- backward-compatible clients
- manual recovery if automatic advancement is disabled or fails before being fully rolled out

#### Approval flow endpoint

Extend:

- `GET /api/v1/devops/remediationtickets/{ticket_id}/approval_flow`

Return node metadata and action hints so the frontend can render the right controls without local workflow inference.

### Compatibility Strategy

The migration should be additive first.

Phase 1:

- add metadata to backend workflow definition
- include metadata in approval flow response
- keep frontend behavior unchanged

Phase 2:

- make frontend consume `requires_input` / `action`
- keep old submit fallback path

Phase 3:

- enable backend auto-advance for no-input phases
- frontend stops calling background submit for `approve` actions

Phase 4:

- enable approve-with-input for phases that require input
- frontend stops calling `submit` after `approve_with_input`

Phase 5:

- after all clients are migrated, decide whether to deprecate no-input submit behavior or the general submit-after-approve path

### Suggested Delivery Order

1. Add phase metadata in `pkg/service/remediationticket/phase` or workflow definition layer.
2. Extend `RemediationApprovalFlowNode` and `GetApprovalFlow`.
3. Update actor transitions so `ticket.phase` is set to the approval target when the approval is created.
4. Add unit tests for metadata projection.
5. Add backend auto-advance behind a config flag, for example `remediationTicket.workflow.autoAdvanceNoInputPhases`.
6. Extend `ApproveTicketPhase` to accept and validate target-phase input before finishing approval.
7. Add tests for approve events that pass through no-input phases and approve events that include input.
8. Update frontend to consume backend action hints.
9. Remove frontend background submit orchestration once backend auto-advance and approve-with-input are enabled.

## Test Design

### Functional Tests

Add backend unit tests for:

- phase metadata defaults
- approval flow returns `requires_input`, `auto_advance`, and `action`
- approving into an input phase stops at `pending`
- approving into a no-input phase advances to the next approval
- approving into final phase reaches `approved` without calling submit
- approving into an input phase with approve input validates, persists input, and creates the next approval
- approving into an input phase without required input fails before finishing the approval task
- creating a new approval sets `ticket.phase` to the target approval node

### Scenario Tests

Cover representative flows:

1. `rdg_sla_owner -> global_deal_desk`
   - target phase requires input
   - approve with valid input should persist the input and create the next approval
   - approve without required input should fail before finishing the approval task

2. `global_deal_desk -> rdg_final_decision_maker -> regional_gm`
   - `rdg_final_decision_maker` has no input
   - approve should auto-advance and create the next approval

3. `rdg_final_decision_maker -> commercial_operation -> regional_gm`
   - optional no-input phase exists
   - workflow should not loop and should respect branch conditions

4. `regional_gm -> succeed`
   - final transition
   - approve should complete the ticket without visible submit

5. Multi-approver approval
   - first approver records task result
   - auto-advance must not happen until approval service status is approved

### Compatibility Tests

- Existing clients can still call `submit` for no-input phases during migration.
- Existing `GetTicket` response schema remains unchanged, but `ticket.phase` semantics migrate to "current workflow node".
- Existing approval records and remediation ticket records do not require migration.
- Approval flow response only adds fields; existing frontend consumers should ignore them safely.
- New clients can stop calling `submit` after `approve` because backend approval handling owns both no-input advancement and approve-with-input submission.

## Impacts and Risks

### Impacts

- Frontend can render remediation actions from backend-provided truth.
- User-facing no-input phases become one-step approve actions.
- User-facing input phases can become true one-step approve-and-submit actions.
- Backend owns workflow advancement, reducing client-side orchestration and race conditions.
- Future phase changes can be expressed through metadata and state machine behavior more cleanly.

### Risks

- Automatic advancement may create more than one state transition in a single approve request, so logs and audit messages must remain clear.
- A bad workflow definition could cause an advancement loop; implementation must bound loop count by phase count.
- If phase metadata is wrong, frontend action rendering may be wrong. Tests must lock metadata for all current phases.
- Background advancement changes timing: downstream consumers that assumed a ticket would pause at every phase may need to consume approval flow rather than raw ticket status.

## Unresolved Questions

1. Should `requires_input` be derived from phase handlers, from workflow config, or from a separate metadata registry?
2. Should backend return `input_fields` in the first implementation, or only `requires_input` and `action`?
3. Should automatic advancement be enabled by default or gated by config for the first rollout?
4. Should no-input `submit` remain permanently supported as an idempotent operation?
5. How should audit logs represent auto-advanced no-input phases: one combined entry or one entry per internal transition?
