# Workflow Module — Architecture Map

> **Last updated:** 2026-06-10
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Workflow module provides a **generic state machine engine** for the ERP. It manages:
- Workflow definition storage (JSON-based state/transition graphs)
- Instance creation and lifecycle tracking per entity
- Transition execution with guard evaluation (permission, role, condition, timeout)
- Transition logging with full audit trail
- Automatic timeout-based transitions (cron-driven)
- Post-transition action execution (event dispatch, notification triggers)
- Visual workflow diagram rendering
- Workflow definition editing (state labels, transition triggers)

It does **NOT** directly manage:
- Business logic of any specific workflow (that lives in each module)
- Status field updates on entities (modules listen to workflow.transitioned events)
- UI for status changes (each module provides its own buttons/forms)

---

## 2. Directory & File Structure

```
app/Modules/Workflow/
├── bootstrap.php                        # Menu + permission registration
├── Routes.php                           # 8 web routes
├── provides.php                         # ModuleRegistry service binding
├── WorkflowEngineAdapter.php            # Implements WorkflowEngineInterface
├── Controllers/
│   └── WorkflowController.php          # Index, diagram, edit, instances, transitions, timeouts
├── Models/
│   ├── WorkflowDefinition.php          # Definition model (JSON parsing, state/transition helpers)
│   ├── WorkflowInstance.php            # Instance model (entity binding, state tracking)
│   └── WorkflowTransitionLog.php       # Transition history model
├── Services/
│   ├── WorkflowEngine.php             # Core engine: createInstance, transition, timeouts
│   └── WorkflowGuardEvaluator.php     # Guard evaluation: permission, role, condition, timeout, custom
└── Views/
    ├── index.php                       # Workflow definitions overview with instance stats
    ├── diagram.php                     # Visual state machine diagram (Mermaid.js)
    ├── edit.php                        # Edit definition (state labels, transition triggers)
    └── instance.php                    # Single instance detail with history + available transitions
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `workflow_definitions` Table (12 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| workflow_code | varchar(50) | NO | UNI | Unique workflow identifier |
| name_ar | varchar(200) | NO | | Arabic name |
| name_en | varchar(200) | YES | | English name |
| description_ar | text | YES | | Arabic description |
| definition_json | json | NO | | Full state machine definition |
| version | int unsigned | NO | | Default: 1, incremented on edit |
| is_active | tinyint(1) | NO | | Default: 1 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |

**Active workflow definitions:**
| Code | Name | Version |
|------|------|---------|
| new_membership | تسجيل عضوية | 1 |
| addition | إضافة (زوج/أبناء/مؤقت) | 1 |
| annual_subscription | الاشتراك السنوي | 1 |
| transfer_separation | تحويل / فصل | 1 |
| violation_penalty | المخالفات والعقوبات | 1 |
| hr_leave_approval | إجازة الموظفين | 1 |
| hr_contract_approval | إجازة العقود | 1 |
| hr_loan_approval | إجازة سلفة الموظفين | 1 |
| hr_end_of_service | إنهاء الخدمة | 1 |
| hr_disciplinary | الإجراءات التأديبية | 1 |
| procurement_pr_approval | إجازة أوامر الشراء | 1 |
| procurement_rtv_approval | إجازة المرتجعات للموردين | 1 |

### 3.2 `workflow_instances` Table (0 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| workflow_definition_id | bigint unsigned | NO | MUL | FK to workflow_definitions |
| workflow_code | varchar(50) | NO | | Denormalized for fast queries |
| entity_type | varchar(100) | NO | MUL | Table/entity name (e.g. 'members') |
| entity_id | bigint unsigned | NO | | Row ID in the entity table |
| current_state | varchar(50) | NO | MUL | Current state key |
| state_entered_at | timestamp | NO | | When current state was entered |
| state_data_json | json | YES | | Arbitrary state payload |
| is_completed | tinyint(1) | NO | MUL | 0=active, 1=terminal state reached |
| completed_at | timestamp | YES | | When completed |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.3 `workflow_transition_log` Table (0 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| workflow_instance_id | bigint unsigned | NO | MUL | FK to workflow_instances |
| from_state | varchar(50) | NO | | State before transition |
| to_state | varchar(50) | NO | | State after transition |
| transition_name | varchar(100) | NO | | Transition identifier |
| triggered_by_employee_id | bigint unsigned | YES | | FK to employees (null for auto) |
| trigger_type | varchar(30) | NO | | manual/timeout/automatic/system |
| guard_results_json | json | YES | | Results of guard evaluations |
| notes | text | YES | | Operator notes |
| created_at | timestamp | NO | MUL | |

---

## 4. Workflow Definition JSON Structure

```json
{
  "states": {
    "state_key": {
      "type": "initial|normal|terminal",
      "label_ar": "...",
      "label_en": "..."
    }
  },
  "transitions": [
    {
      "name": "transition_name",
      "from": "state_key",
      "to": "state_key",
      "trigger": "manual|automatic",
      "guards": [
        {"type": "permission|role|condition|timeout", "value": "...", "days": N}
      ],
      "actions": [
        {"type": "event|notification", "value": "event.name|template_code"}
      ]
    }
  ]
}
```

---

## 5. Guard Types

| Type | What it checks | Evaluator |
|------|---------------|-----------|
| permission | Current employee has specified permission key | `employee->hasPermission()` |
| role | Current employee has specified role code | `employee->hasRole()` |
| condition | Named condition (built-in or registered via ActionRegistry) | see below |
| timeout | Days elapsed since entering current state | `state_entered_at + days <= now` |
| (custom) | Any type registered via ActionRegistry as `guard_type.{type}` | Callable handler |

### Built-in Conditions:
| Condition | Logic |
|-----------|-------|
| has_payment_receipt | Checks payments table for non-voided payment linked to entity |
| is_subscription_paid | Checks subscriptions table for current year |
| within_appeal_window | state_entered_at + 15 days (configurable via APPEAL_WINDOW rule) |
| five_years_unpaid | 5+ consecutive years with unpaid subscriptions |

### Fallback behavior:
- Unknown guards with no registered handler **pass by default** (to not block workflows)
- Unknown conditions check `state_data_json` for a boolean flag matching the condition name

---

## 6. Events Dispatched

| Event | Payload | Dispatched When |
|-------|---------|-----------------|
| workflow.created | {instance_id, workflow_code, entity_type, entity_id, current_state} | New instance created |
| workflow.transitioned | {instance_id, workflow_code, entity_type, entity_id, from_state, to_state, transition_name, trigger_type, is_completed} | After successful transition |
| (custom via actions) | {workflow_instance_id, workflow_code, entity_type, entity_id, new_state} | When transition has action type='event' |
| notification.send | {template, entity_type, entity_id} | When transition has action type='notification' |

---

## 7. Events Consumed

None directly via EventBus — the Workflow module is invoked programmatically by other modules.

---

## 8. Cross-Module Dependencies

### 8.1 Workflow module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Core | App, EventBus, Logger, Model, ActionRegistry |

### 8.2 Other modules that IMPORT FROM Workflow:

| Module | What it imports | How |
|--------|----------------|-----|
| Members | WorkflowEngine | Creates 'new_membership' instance on member creation, transitions on status changes |
| Fines | WorkflowEngine, WorkflowInstance | 'violation_penalty' workflow for fine/violation processes |
| HR (Leave) | WorkflowEngine | 'hr_leave_approval' workflow |
| HR (Contract) | WorkflowEngine | 'hr_contract_approval' workflow |
| HR (Loan) | WorkflowEngine | 'hr_loan_approval' workflow |
| HR (End of Service) | WorkflowEngine | 'hr_end_of_service' workflow |
| HR (Disciplinary) | WorkflowEngine | 'hr_disciplinary' workflow |
| Interviews | WorkflowEngine | Interview scheduling workflow |
| Payments | (bootstrap.php references) | Payment lifecycle workflow |

### 8.3 Service Binding (provides.php):

```php
ModuleRegistry::register('workflow', [
    'services' => [WorkflowEngineInterface::class],
    'description' => 'State machine — workflow instances, transitions, guards',
]);
```

The `WorkflowEngineAdapter` implements `App\Core\Contracts\WorkflowEngineInterface`, providing a clean interface for other modules:
- `createInstance(code, entityType, entityId, stateData)`
- `transition(instanceId, transitionName, notes, triggerType)`
- `transitionByEntity(entityType, entityId, transitionName, notes)`
- `getAvailableTransitions(instanceId)`
- `getCurrentState(entityType, entityId, workflowCode)`
- `getHistory(instanceId)`

---

## 9. Routes (8)

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /workflows | WorkflowController@index | rules.view |
| GET | /workflows/diagram/{code} | WorkflowController@diagram | rules.view |
| GET | /workflows/edit/{code} | WorkflowController@edit | rules.edit |
| POST | /workflows/edit/{code} | WorkflowController@update | rules.edit |
| GET | /workflows/instances/{id} | WorkflowController@instance | rules.view |
| POST | /workflows/instances/{id}/transition | WorkflowController@transition | rules.edit |
| GET | /workflows/instances/{code} | WorkflowController@instances | rules.view |
| POST | /workflows/process-timeouts | WorkflowController@processTimeouts | rules.edit |

**Note**: Route permissions use `rules.view` and `rules.edit` — these are registered by the Rules module, not the Workflow module itself. The Workflow module registers `workflow.view`, `workflow.transition`, `workflow.manage` permissions but the routes reference different permission keys.

---

## 10. Permissions

| Key | Description |
|-----|-------------|
| workflow.view | View workflows (registered but not used in routes) |
| workflow.transition | Execute transitions (registered but not used in routes) |
| workflow.manage | Manage workflow definitions (registered but not used in routes) |

**Actually used in routes**: `rules.view`, `rules.edit` (from Rules module)

---

## 11. Background Processes (Cron)

### WorkflowEngine::processTimeouts()
- Scans all active workflow definitions for transitions with `trigger: 'automatic'` and `timeout` guards
- Finds instances stuck in the source state longer than the timeout days
- Automatically executes the transition with `trigger_type = 'timeout'`
- Notes the transition as "انتقال تلقائي بسبب انتهاء المهلة" (automatic transition due to timeout)
- Triggered via `POST /workflows/process-timeouts` (manual) or cron runner

---

## 12. High-Risk Areas

### 12.1 No Active Instances (UNUSED IN PRACTICE)
- The `workflow_instances` table has 0 rows despite 12 definitions existing
- This suggests workflow integration is defined but not yet active in production flows
- Members module wraps workflow calls in try/catch — workflow failures are silently swallowed

### 12.2 Permission Mismatch
- Routes use `rules.view` / `rules.edit` permissions
- Registered permissions are `workflow.view` / `workflow.transition` / `workflow.manage`
- If someone has `workflow.manage` but not `rules.edit`, they cannot edit workflows despite having the "correct" permission

### 12.3 Unknown Guards Pass by Default
- `evaluateCustom()` returns `true` for unregistered guard types
- This means a misconfigured guard will NOT block transitions
- Could allow unintended state transitions if guard handlers are not registered at boot time

### 12.4 Transaction Scope
- Transition execution (instance update + log insert) is wrapped in a DB transaction
- Post-transition actions (event dispatch, notification) run AFTER commit
- If actions fail, the transition is already committed — no rollback

### 12.5 No Rollback/Undo Mechanism
- Once a transition executes, there is no built-in way to reverse it
- Manual intervention would require direct DB manipulation
- No "revert to previous state" feature exists

### 12.6 Definition Versioning is Informational Only
- Version number increments on edit but old instances are not migrated
- If a definition's states/transitions change, existing instances using the old structure may become inconsistent
- No version pinning per instance (instance references definition by ID, not by version)

---

## 13. Known Patterns & Gotchas

1. **Optional integration**: All modules that use WorkflowEngine wrap calls in try/catch — if the workflow engine fails or a definition doesn't exist, the business flow continues without workflow tracking
2. **Entity-centric lookup**: `findForEntity(entityType, entityId)` finds the most recent non-completed instance — supports only ONE active workflow per entity per workflow_code
3. **ensureInstance()**: Creates a new instance if one doesn't exist — useful for retroactive workflow attachment
4. **State data**: `state_data_json` can hold arbitrary key-value pairs that condition guards can check (e.g., `receipt_number` flag)
5. **Mermaid.js diagrams**: The diagram view renders workflow definitions as interactive Mermaid flowcharts
6. **Index view shows filtered instances**: When accessing `/workflows/instances/{code}`, it reuses the index view with `filteredInstances` data
7. **WorkflowEngineAdapter**: Thin adapter implementing `WorkflowEngineInterface` — all methods delegate to `WorkflowEngine` static methods. This allows dependency injection without importing the concrete class.
