# RejectApprovalStep

## Permission Scope

request

## Overview

RejectApprovalStep records a reject decision against a single `ApprovalStepAssignee` row that the calling actor owns and cascades the rejection to the parent step and request immediately. The command requires a non-empty `reason` and writes the same string to both `ApprovalDecision.comment` and `ApprovalRequest.rejectionReason` in the same transaction so that list-view queries can surface the reason without joining the decision log. Any reject — whether from a `required = true` or `required = false` assignee — fails the step immediately and cascades the request to `REJECTED`; trailing steps remain `PENDING` (no `SKIPPED` state).

## Business Rules

- The calling actor (`ctx.actorId`) must own the target `ApprovalStepAssignee` row
- The target `ApprovalStepAssignee.status` must be `PENDING`
- The parent `ApprovalStep.status` must be `IN_PROGRESS`
- The parent `ApprovalRequest.status` must be `PENDING` (parent-Request liveness guard)
- Self-rejection is blocked: `ctx.actorId` cannot equal `ApprovalRequest.requesterId` (segregation of duties applies to all step-level outcomes)
- A non-empty `reason` is required and is written verbatim to `ApprovalDecision.comment` and `ApprovalRequest.rejectionReason`
- Exactly one `ApprovalDecision` row is written with `decision = REJECT`, `approvalRequestId` and `approvalStepId` both set, `decidedByUserId = ctx.actorId`, and `comment = reason`
- The assignee row transitions to `REJECTED` and `resolvedAt` is set
- The parent step transitions to `REJECTED` and `resolvedAt` is set, regardless of whether the rejecting assignee was `required = true` or `required = false` and regardless of remaining `PENDING` assignees
- The parent request transitions to `REJECTED` and `resolvedAt` is set; trailing `ApprovalStep` rows remain `PENDING` (no `SKIPPED` state)

## Process Flow

```mermaid
flowchart TD
    A[Receive reject request] --> B[Load StepAssignee + parent Step + Request<br/>with row lock]
    B --> C{Assignee belongs to ctx.actorId<br/>and StepAssignee.status = PENDING<br/>and Step.status = IN_PROGRESS?}
    C -->|No| CX[Return error: NOT_ACTIVE_ASSIGNEE]
    C -->|Yes| D{Request.status = PENDING?}
    D -->|No| DX[Return error: PARENT_REQUEST_NOT_PENDING]
    D -->|Yes| E{ctx.actorId === Request.requesterId?}
    E -->|Yes| EX[Return error: SELF_DECISION_NOT_ALLOWED]
    E -->|No| F{reason non-empty?}
    F -->|No| FX[Return error: MISSING_REQUIRED_FIELD]
    F -->|Yes| G[Insert ApprovalDecision:<br/>decision = REJECT, both ids set,<br/>decidedByUserId = ctx.actorId,<br/>comment = reason]
    G --> H[Update StepAssignee:<br/>status = REJECTED, resolvedAt = now]
    H --> I[Update Step: status = REJECTED, resolvedAt = now]
    I --> J[Update Request: status = REJECTED,<br/>rejectionReason = reason,<br/>resolvedAt = now]
    J --> K[Trailing steps remain PENDING<br/>no SKIPPED state]
    K --> L[Return rejected request]
```

## External Dependencies

- None

## Error Scenarios

- **NOT_ACTIVE_ASSIGNEE**: The actor does not own a `PENDING` `ApprovalStepAssignee` row on an `IN_PROGRESS` `ApprovalStep`
- **PARENT_REQUEST_NOT_PENDING**: The parent `ApprovalRequest.status` is not `PENDING`
- **SELF_DECISION_NOT_ALLOWED**: The actor is the requester of the parent request; segregation of duties prohibits self-decision on APPROVE / REJECT / SEND_BACK / DELEGATE
- **MISSING_REQUIRED_FIELD**: A required input field is missing or empty

## Test Cases

- rejects a step from a required = true assignee and cascades the request to REJECTED
- rejects a step from a required = false (optional) assignee and cascades the request to REJECTED immediately, regardless of remaining assignees
- writes the reason verbatim to ApprovalDecision.comment and ApprovalRequest.rejectionReason in the same transaction
- writes one ApprovalDecision row with decision = REJECT, both ids set
- transitions assignee, step, and request rows to REJECTED and stamps resolvedAt on each
- leaves trailing steps in PENDING (no SKIPPED state) — never updates ApprovalStep with status PENDING
- throws MISSING_REQUIRED_FIELD when reason is empty
- throws NOT_ACTIVE_ASSIGNEE when the assignee row is missing
- throws NOT_ACTIVE_ASSIGNEE when the actor does not own the assignee
- throws NOT_ACTIVE_ASSIGNEE when the assignee status is not PENDING
- throws NOT_ACTIVE_ASSIGNEE when the parent step is not IN_PROGRESS
- throws PARENT_REQUEST_NOT_PENDING when the parent request is in any non-PENDING status
- throws SELF_DECISION_NOT_ALLOWED when the actor is the requester
- a reject blocked by SoD or parent-Request liveness writes nothing and does not cascade
