# WithdrawApprovalRequest

## Permission Scope

request

## Overview

WithdrawApprovalRequest lets the original requester voluntarily terminate their own request before it resolves — whether it is still `PENDING` or has been sent back to them as `REVISION_REQUESTED`. The command writes one `ApprovalDecision` row with `decision = WITHDRAW`, `approvalRequestId` set, and `approvalStepId` null, then transitions the request to `WITHDRAWN` and stamps `resolvedAt`. Step and assignee rows are deliberately left untouched — the parent `ApprovalRequest.status = WITHDRAWN` is the authoritative liveness signal that inbox queries and decision-recording guards rely on to suppress further activity.

## Business Rules

- Only the original requester (`ctx.actorId === ApprovalRequest.requesterId`) may withdraw the request
- The target request must be in `PENDING` or `REVISION_REQUESTED` status; withdraw is rejected on every terminal state
- Exactly one `ApprovalDecision` row is written with `decision = WITHDRAW`, `approvalRequestId` set, `approvalStepId = null`, `decidedByUserId = ctx.actorId`, and an optional `comment`
- The request transitions to `WITHDRAWN` 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 withdrawn request are rejected by the parent-Request liveness guard in `approveApprovalStep` / `rejectApprovalStep` / `delegateApprovalStep`

## Process Flow

```mermaid
flowchart TD
    A[Receive withdraw 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{ctx.actorId === Request.requesterId?}
    D -->|No| DX[Return error: NOT_REQUESTER]
    D -->|Yes| E[Insert ApprovalDecision:<br/>decision = WITHDRAW,<br/>approvalRequestId set,<br/>approvalStepId = null,<br/>decidedByUserId = ctx.actorId,<br/>comment optional]
    E --> F[Update request: status = WITHDRAWN,<br/>resolvedAt = now]
    F --> G[Step and assignee rows are not modified]
    G --> H[Return withdrawn 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
- **NOT_REQUESTER**: `ctx.actorId` does not match `ApprovalRequest.requesterId`

## Test Cases

- transitions PENDING to WITHDRAWN and stamps resolvedAt
- transitions REVISION_REQUESTED to WITHDRAWN and stamps resolvedAt
- writes an ApprovalDecision row with decision = WITHDRAW, approvalStepId null, and approvalRequestId set
- records ctx.actorId on the decision row's decidedByUserId
- accepts an optional comment and stores it on the decision row when supplied
- throws NOT_REQUESTER when called by a non-requester user
- 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 CANCELLED
- throws INVALID_STATUS_TRANSITION when the request is already WITHDRAWN
- throws REQUEST_NOT_FOUND for a non-existent approvalRequestId
