# CancelApprovalRequest

## Permission Scope

request

## Overview

CancelApprovalRequest is the escape hatch for terminating an in-flight request — `PENDING` or `REVISION_REQUESTED` — that the requester themselves cannot or will not withdraw, for example a stuck request whose assignees are all unavailable or whose requester has left the company. The command writes one `ApprovalDecision` row with `decision = CANCEL`, `approvalRequestId` set, `approvalStepId` null, and the supplied `reason` as `comment`, then transitions the request to `CANCELLED` and stamps `resolvedAt`. Step and assignee rows are deliberately left untouched.

## Business Rules

- A non-empty `reason` is required and is written verbatim to `ApprovalDecision.comment`
- The target request must be in `PENDING` or `REVISION_REQUESTED` status; cancel is rejected on every terminal state
- Exactly one `ApprovalDecision` row is written with `decision = CANCEL`, `approvalRequestId` set, `approvalStepId = null`, `decidedByUserId = ctx.actorId`, and `comment = reason`
- The request transitions to `CANCELLED` and `resolvedAt` is set within the same transaction
- `ApprovalStep.status` and `ApprovalStepAssignee.status` are not modified — they retain their pre-termination values
- Late-arriving step-level decisions on a cancelled request are rejected by the parent-Request liveness guard

## Process Flow

```mermaid
flowchart TD
    A[Receive cancel request] --> B{Request exists?}
    B -->|No| BX[Return error: REQUEST_NOT_FOUND]
    B -->|Yes| C{Request.status in PENDING, REVISION_REQUESTED?}
    C -->|No| CX[Return error: INVALID_STATUS_TRANSITION]
    C -->|Yes| D{reason non-empty?}
    D -->|No| DX[Return error: MISSING_REQUIRED_FIELD]
    D -->|Yes| E[Insert ApprovalDecision:<br/>decision = CANCEL,<br/>approvalRequestId set,<br/>approvalStepId = null,<br/>decidedByUserId = ctx.actorId,<br/>comment = reason]
    E --> F[Update request: status = CANCELLED,<br/>resolvedAt = now]
    F --> G[Step and assignee rows are not modified]
    G --> H[Return cancelled request]
```

## External Dependencies

- None

## Error Scenarios

- **REQUEST_NOT_FOUND**: Specified `approvalRequestId` does not exist
- **INVALID_STATUS_TRANSITION**: Target entity is not in a status that permits this operation
- **MISSING_REQUIRED_FIELD**: A required input field is missing or empty

## Test Cases

- transitions PENDING to CANCELLED and stamps resolvedAt
- transitions REVISION_REQUESTED to CANCELLED and stamps resolvedAt
- writes an ApprovalDecision row with decision = CANCEL, approvalStepId null, and approvalRequestId set
- records ctx.actorId on the decision row's decidedByUserId
- writes the supplied reason to the decision row's comment
- throws MISSING_REQUIRED_FIELD when reason is empty
- throws INVALID_STATUS_TRANSITION when the request is APPROVED
- throws INVALID_STATUS_TRANSITION when the request is REJECTED
- throws INVALID_STATUS_TRANSITION when the request is WITHDRAWN
- throws INVALID_STATUS_TRANSITION when the request is already CANCELLED
- throws REQUEST_NOT_FOUND for a non-existent approvalRequestId
