# CreateApprovalPolicy

## Permission Scope

policy

## Overview

CreateApprovalPolicy registers a new reusable approval template under a given `purpose` and `name` and persists it together with its ordered steps and per-step assignees in a single transaction. The new policy is created in `DRAFT` status with `activatedAt` null; activation timestamping is deferred to `activateApprovalPolicy`. The command accepts an inline `steps` array where each step carries `stepOrder`, `name`, `minimumApprovals` (default 1), and an `assignees` array. Each assignee row sets exactly one of `userId` or `roleId`; user assignees may set `required` to mark the user as mandatory, and role assignees may set `roleQuorum` (`ALL` / `ANY` / null) to express group-level voting semantics.

## Business Rules

- `purpose` is a non-empty opaque string and is not validated against an enum
- `name` is required and non-empty
- The policy must contain at least one step
- Within a single policy, `stepOrder` is unique across all supplied steps
- Each step must contain at least one assignee
- For each step, `minimumApprovals` is a positive integer; defaults to 1 when not supplied. The relationship between `minimumApprovals` and runtime assignee count (after role expansion) is enforced at step activation, not at policy creation
- Each assignee row sets exactly one of `userId` or `roleId` (XOR); rows that set both or neither are rejected
- For user assignees (`userId` set), `roleQuorum` must be null; setting `roleQuorum` is rejected
- For role assignees (`roleId` set), `required` must be false; setting `required = true` is rejected (group-level mandatory semantics are expressed via `roleQuorum = 'ALL'` instead)
- Within a single step, `roleId` is unique among role assignees; two role rows on the same step targeting the same role are rejected
- The new policy is created in `DRAFT` status with `activatedAt = null`
- Multiple `DRAFT` policies under the same `(purpose, name)` are allowed (administrators may stage successor variants)

## Process Flow

```mermaid
flowchart TD
    A[Receive create request] --> B{Validate name and purpose<br/>are non-empty}
    B -->|Invalid| BX[Return error: MISSING_REQUIRED_FIELD]
    B -->|Valid| C{At least one step?}
    C -->|No| CX[Return error: POLICY_HAS_NO_STEPS]
    C -->|Yes| D{stepOrder unique<br/>within policy?}
    D -->|No| DX[Return error: DUPLICATE_STEP_ORDER]
    D -->|Yes| E{Every step has<br/>at least one assignee?}
    E -->|No| EX[Return error: STEP_HAS_NO_ASSIGNEES]
    E -->|Yes| F{Every step's minimumApprovals<br/>is a positive integer?}
    F -->|No| FX[Return error: INVALID_MINIMUM_APPROVALS]
    F -->|Yes| G{Each assignee has exactly<br/>one of userId or roleId?}
    G -->|No| GX[Return error: ASSIGNEE_USER_ROLE_XOR_VIOLATED]
    G -->|Yes| G2{required only on user assignees,<br/>roleQuorum only on role assignees?}
    G2 -->|No| G2X[Return error: ASSIGNEE_FLAGS_INCOMPATIBLE]
    G2 -->|Yes| G3{userId and roleId unique among<br/>assignees per step?}
    G3 -->|No| G3X[Return error: DUPLICATE_USER_ASSIGNEE / DUPLICATE_ROLE_ASSIGNEE]
    G3 -->|Yes| H[Insert ApprovalPolicy with status DRAFT, activatedAt null]
    H --> I[Bulk-insert ApprovalPolicyStep rows]
    I --> J[Bulk-insert ApprovalPolicyStepAssignee rows]
    J --> K[Return created policy]
```

## External Dependencies

- None

## Error Scenarios

- **MISSING_REQUIRED_FIELD**: A required input field is missing or empty
- **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`

## Test Cases

- creates a policy with status DRAFT, activatedAt null, and one step with one assignee
- creates a policy with multiple steps in distinct stepOrder values
- creates a policy with multiple assignees per step
- defaults minimumApprovals to 1 when not supplied
- accepts a step with minimumApprovals = 2 and one required = true assignee as a valid configuration
- accepts purpose values such as "PRODUCT_ACTIVATION" or "PO_CONFIRMATION" verbatim with no enum validation
- allows multiple DRAFT policies under the same (purpose, name) pair
- throws MISSING_REQUIRED_FIELD when name is empty
- throws MISSING_REQUIRED_FIELD when purpose is empty
- throws POLICY_HAS_NO_STEPS when steps array is empty
- throws DUPLICATE_STEP_ORDER when two steps share a stepOrder
- throws STEP_HAS_NO_ASSIGNEES when a step has no assignees
- throws INVALID_MINIMUM_APPROVALS when minimumApprovals is zero or negative
- accepts minimumApprovals greater than the policy assignee row count (the relationship is validated at step activation after role expansion)
- throws ASSIGNEE_USER_ROLE_XOR_VIOLATED when an assignee row sets both userId and roleId
- throws ASSIGNEE_USER_ROLE_XOR_VIOLATED when an assignee row sets neither userId nor roleId
- accepts a role assignee with roleQuorum = ALL as a valid configuration
- accepts a role assignee with roleQuorum = ANY as a valid configuration
- accepts a role assignee with roleQuorum = null (only step.minimumApprovals applies)
- throws ASSIGNEE_FLAGS_INCOMPATIBLE when a role assignee sets required = true
- throws ASSIGNEE_FLAGS_INCOMPATIBLE when a user assignee sets roleQuorum
- throws DUPLICATE_USER_ASSIGNEE when two user assignees within the same step share the same userId
- throws DUPLICATE_ROLE_ASSIGNEE when two role assignees within the same step share the same roleId
