# CreateApprovalRequest

## Permission Scope

request

## Overview

CreateApprovalRequest persists a new `ApprovalRequest` together with its runtime step and assignee rows in `PENDING` status, activates the lowest-`stepOrder` step, and expands any role-based assignees on that step into per-ACTIVE-member rows. The command supports two creation modes. In policy mode the caller passes a `policyId`; the module reads the source `ApprovalPolicy` (which must be `ACTIVE`), copies its policy step and assignee structure into the request's runtime `ApprovalStep` and `ApprovalStepAssignee` rows, and records `sourcePolicyId`. In direct mode the caller passes inline `steps`; the module persists the supplied subtree verbatim and leaves `sourcePolicyId` null. The new request is immediately ready to receive step-level decisions; per-target exclusivity is the wrapper command's responsibility via `getActiveApprovalRequest`.

## Business Rules

- Exactly one of `policyId` or `steps` must be supplied; rejecting both-or-neither
- `name` is required and non-empty
- `purpose` is a non-empty opaque string and is not validated against an enum
- `targetEntityType` (free string) and `targetEntityId` (UUID) are required and stored as-is; no foreign-key constraint is enforced against the target
- `requesterId` is set from `ctx.actorId` and cannot be supplied by the caller
- In policy mode, the source `ApprovalPolicy.status` must be `ACTIVE` at creation time; INACTIVE or DRAFT policies are rejected
- In direct mode, the supplied `steps` array must satisfy the same shape rules as `createApprovalPolicy`: at least one step, unique `stepOrder`, every step has at least one assignee, valid `minimumApprovals`, and assignee `userId` XOR `roleId`
- The request maintains its own runtime step and assignee rows and never re-reads the source policy after creation; subsequent deactivation or replacement of the policy does not affect the in-flight request
- The new request is created with `status = PENDING`, `resolvedAt = null`, `rejectionReason = null`
- The lowest-`stepOrder` `ApprovalStep` is created with `status = IN_PROGRESS`; trailing steps are created with `status = PENDING`
- For each role-based assignee on the activating step, expansion calls `listUsersByRole({ roleId })` for the current ACTIVE membership; one `ApprovalStepAssignee` per ACTIVE member is materialized with the originating `roleId` and `roleQuorum` propagated verbatim, and the role-only seed row is omitted
- A role with zero current ACTIVE members on the activating step fails loud rather than silently auto-completing the step
- After role expansion, the activating step's runtime assignee count must be no less than the step's `minimumApprovals`; otherwise creation fails loud
- Role-based assignee rows on trailing steps are persisted with `roleId` set; expansion is deferred to the step's `PENDING → IN_PROGRESS` transition

## Process Flow

```mermaid
flowchart TD
    A[Receive create request] --> B{Exactly one of<br/>policyId or steps supplied?}
    B -->|No| BX[Return error: CREATION_MODE_AMBIGUOUS]
    B -->|Yes| C{Mode}
    C -->|policyId| D[Load ApprovalPolicy with steps + assignees]
    D --> D1{Policy exists?}
    D1 -->|No| D1X[Return error: POLICY_NOT_FOUND]
    D1 -->|Yes| D2{Policy.status = ACTIVE?}
    D2 -->|No| D2X[Return error: POLICY_NOT_ACTIVE]
    D2 -->|Yes| E[Copy template into request runtime rows]
    C -->|steps| F[Validate steps shape<br/>same rules as createApprovalPolicy]
    F -->|Invalid| FX[Return shape-specific error]
    F -->|Valid| F2[Persist supplied subtree verbatim;<br/>sourcePolicyId = null]
    E --> G[Insert ApprovalRequest with status PENDING<br/>requesterId = ctx.actorId,<br/>sourcePolicyId = policy.id]
    F2 --> G2[Insert ApprovalRequest with status PENDING<br/>requesterId = ctx.actorId,<br/>sourcePolicyId = null]
    G --> H[Identify lowest-stepOrder step;<br/>materialize role assignees via listUsersByRole]
    G2 --> H
    H --> H1{Role has at least one<br/>ACTIVE member?}
    H1 -->|No| H1X[Return error: ROLE_HAS_NO_ACTIVE_MEMBERS]
    H1 -->|Yes| H2{Runtime assignee count<br/>>= step.minimumApprovals?}
    H2 -->|No| H2X[Return error: STEP_QUORUM_UNREACHABLE]
    H2 -->|Yes| I[Mark activating step IN_PROGRESS]
    I --> J[Return created request]
```

## External Dependencies

- [user-management::listUsersByRole](../../../user-management/docs/query/ListUsersByRole.md) - Resolve current ACTIVE role members at activation time

## Error Scenarios

- **CREATION_MODE_AMBIGUOUS**: Both `policyId` and `steps` were supplied, or neither was supplied
- **MISSING_REQUIRED_FIELD**: A required input field is missing or empty
- **POLICY_NOT_FOUND**: Specified `policyId` does not exist
- **POLICY_NOT_ACTIVE**: Source policy is not in `ACTIVE` status (policy mode only)
- **POLICY_HAS_NO_STEPS**: The supplied `steps` array is empty
- **DUPLICATE_STEP_ORDER**: Two or more steps share the same `stepOrder`
- **STEP_HAS_NO_ASSIGNEES**: A step has no assignees
- **INVALID_MINIMUM_APPROVALS**: A step's `minimumApprovals` is not a positive integer
- **ASSIGNEE_USER_ROLE_XOR_VIOLATED**: An assignee row sets both `userId` and `roleId`, or neither
- **ASSIGNEE_FLAGS_INCOMPATIBLE**: An assignee row sets `required = true` on a role assignee, or sets `roleQuorum` on a user assignee
- **DUPLICATE_USER_ASSIGNEE**: Two user assignees within the same step target the same `userId`
- **DUPLICATE_ROLE_ASSIGNEE**: Two role assignees within the same step target the same `roleId`
- **ROLE_HAS_NO_ACTIVE_MEMBERS**: A role-based assignee expanded to zero ACTIVE members at step activation
- **STEP_QUORUM_UNREACHABLE**: A step's runtime assignee count after role expansion is less than its `minimumApprovals` threshold

## Test Cases

- creates a request in policy mode and copies steps and assignees from the source policy
- creates a request in direct mode and persists supplied steps verbatim with sourcePolicyId null
- sets requesterId from ctx.actorId
- accepts targetEntityType and targetEntityId as opaque strings/UUIDs with no FK enforcement
- accepts purpose as a free string with no enum validation
- creates the request with status PENDING and the lowest-stepOrder step set to IN_PROGRESS
- expands a role-based assignee on the activating step into per-ACTIVE-member rows
- propagates the originating roleId and roleQuorum verbatim to every materialized row
- preserves user-only assignees on the activating step verbatim (no expansion needed)
- does not expand role-based assignees on trailing steps; they expand at their own activation moment
- throws CREATION_MODE_AMBIGUOUS when both policyId and steps are supplied
- throws CREATION_MODE_AMBIGUOUS when neither policyId nor steps are supplied
- throws POLICY_NOT_FOUND for a non-existent policyId
- throws POLICY_NOT_ACTIVE when the source policy is in DRAFT
- throws POLICY_NOT_ACTIVE when the source policy is INACTIVE
- throws POLICY_HAS_NO_STEPS when direct-mode steps array is empty
- throws DUPLICATE_STEP_ORDER when two direct-mode steps share the same stepOrder
- throws STEP_HAS_NO_ASSIGNEES when a direct-mode step has no assignees
- throws INVALID_MINIMUM_APPROVALS when minimumApprovals is zero or negative
- throws ASSIGNEE_USER_ROLE_XOR_VIOLATED when an assignee sets neither userId nor roleId
- accepts a direct-mode role assignee with roleQuorum = ALL or ANY as a valid configuration
- throws ASSIGNEE_FLAGS_INCOMPATIBLE when a direct-mode role assignee sets required = true
- throws ASSIGNEE_FLAGS_INCOMPATIBLE when a direct-mode user assignee sets roleQuorum
- throws DUPLICATE_USER_ASSIGNEE when two direct-mode user assignees within the same step share the same userId
- throws DUPLICATE_ROLE_ASSIGNEE when two direct-mode role assignees within the same step share the same roleId
- throws ROLE_HAS_NO_ACTIVE_MEMBERS when a role on the activating step has zero current ACTIVE members
- throws STEP_QUORUM_UNREACHABLE when role expansion yields fewer runtime assignees than the activating step's minimumApprovals
- throws MISSING_REQUIRED_FIELD when name is empty
