# DelegateApprovalStep

## Permission Scope

request

## Overview

DelegateApprovalStep substitutes the calling actor's vote on a step by transferring their seat to another user. The command writes one `ApprovalDecision` row with `decision = DELEGATE` and `delegatedToUserId` populated, transitions the original `ApprovalStepAssignee` row to `DELEGATED` with `resolvedAt` stamped, and inserts a new `ApprovalStepAssignee` row for the delegate with `status = PENDING` and the originating row's `required` flag carried over verbatim. The new row participates in the same step's voting on equal footing with originally-assigned rows. The command rejects self-delegation and circular delegation so that the assignee set always grows toward resolution.

## 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-delegation is blocked: `ctx.actorId === ApprovalRequest.requesterId` (segregation of duties applies to all step-level outcomes)
- `delegatedToUserId !== ctx.actorId` (cannot delegate to oneself)
- `delegatedToUserId` must not already be an active (`PENDING`) `ApprovalStepAssignee` on the same step (no circular delegation)
- Exactly one `ApprovalDecision` row is written with `decision = DELEGATE`, `approvalRequestId` and `approvalStepId` both set, `decidedByUserId = ctx.actorId`, `delegatedToUserId` set, and an optional `comment`
- The original assignee row transitions to `DELEGATED` and `resolvedAt` is set
- A new `ApprovalStepAssignee` row is inserted for the delegate with `userId = delegatedToUserId`, the originating row's `required` flag carried over verbatim, and `status = PENDING`
- Voting evaluation does not advance on a delegate (the original is replaced one-for-one in the assignee set); step status remains `IN_PROGRESS`

## Process Flow

```mermaid
flowchart TD
    A[Receive delegate 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{delegatedToUserId === ctx.actorId?}
    F -->|Yes| FX[Return error: SELF_DELEGATION_NOT_ALLOWED]
    F -->|No| G{delegatedToUserId already a<br/>PENDING assignee on this step?}
    G -->|Yes| GX[Return error: CIRCULAR_DELEGATION]
    G -->|No| H[Insert ApprovalDecision:<br/>decision = DELEGATE, both ids set,<br/>delegatedToUserId set,<br/>decidedByUserId = ctx.actorId,<br/>comment optional]
    H --> I[Update original StepAssignee:<br/>status = DELEGATED, resolvedAt = now]
    I --> J[Insert new StepAssignee:<br/>userId = delegatedToUserId,<br/>required carried over verbatim,<br/>status = PENDING]
    J --> K[Step stays IN_PROGRESS — return]
```

## 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
- **SELF_DELEGATION_NOT_ALLOWED**: `delegatedToUserId` equals `ctx.actorId`
- **CIRCULAR_DELEGATION**: `delegatedToUserId` is already a `PENDING` assignee on the same step

## Test Cases

- delegates an assignee's seat to a peer and inserts a new PENDING StepAssignee for the delegate
- transitions the original assignee row to DELEGATED and stamps resolvedAt
- propagates the originating row's required flag to the new delegate row verbatim
- writes an ApprovalDecision row with decision = DELEGATE, both ids set, and delegatedToUserId populated
- accepts an optional comment and stores it on the decision row
- throws SELF_DELEGATION_NOT_ALLOWED when delegatedToUserId equals ctx.actorId
- throws CIRCULAR_DELEGATION when delegatedToUserId is already PENDING on the same step
- throws SELF_DECISION_NOT_ALLOWED when ctx.actorId is the requester
- throws NOT_ACTIVE_ASSIGNEE when the actor is not an active assignee (assignee not found)
- throws NOT_ACTIVE_ASSIGNEE when the assignee belongs to a different user
- 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 APPROVED
- throws PARENT_REQUEST_NOT_PENDING when the parent request is REJECTED
- throws PARENT_REQUEST_NOT_PENDING when the parent request is WITHDRAWN
- does not update the parent step row — step remains IN_PROGRESS one-for-one
