# GoodBooks GB5 — Workflow System
### Complete Reference: Purpose, Architecture, Configuration, and Usage

---

## Table of Contents

1. [What Is the Workflow System?](#1-what-is-the-workflow-system)
2. [Key Concepts](#2-key-concepts)
3. [System Architecture](#3-system-architecture)
4. [Database Schema](#4-database-schema)
5. [Workflow Definition — Setup](#5-workflow-definition--setup)
6. [Workflow Configuration — Activation Rules](#6-workflow-configuration--activation-rules)
7. [How a Document Enters Workflow](#7-how-a-document-enters-workflow)
8. [The Approval Lifecycle](#8-the-approval-lifecycle)
9. [Assignment Strategies — Who Gets the Task?](#9-assignment-strategies--who-gets-the-task)
10. [Condition Evaluation Engine](#10-condition-evaluation-engine)
11. [WIP (Form-Based Approval) Mode](#11-wip-form-based-approval-mode)
12. [SLA, Due Dates, and Escalation](#12-sla-due-dates-and-escalation)
13. [User Delegation](#13-user-delegation)
14. [Particulars — Display Text](#14-particulars--display-text)
15. [Audit Trail and History](#15-audit-trail-and-history)
16. [API Reference](#16-api-reference)
17. [Admin Setup Guide — Step by Step](#17-admin-setup-guide--step-by-step)
18. [Developer Integration Guide](#18-developer-integration-guide)
19. [Troubleshooting](#19-troubleshooting)
20. [Reference: Enums and Constants](#20-reference-enums-and-constants)

---

## 1. What Is the Workflow System?

The workflow system is a **multi-level approval engine** built into the GB5 backend. Its purpose
is to route business documents (purchase orders, task requests, expense claims, etc.) through
one or more human approval gates before the document is treated as finalised.

### What it does

- Determines **whether a document needs approval** at all (based on entity type, organisational
  unit, transaction type, and dynamic conditions like value or category).
- Routes the document to the **correct approver(s)** at each stage using configurable assignment
  rules.
- Manages the **full lifecycle** of an approval request: pending → approved / rejected / returned.
- Maintains a complete **audit trail** of every action taken by every approver.
- Supports **multi-level sequential** approvals (Level 1 → Level 2 → Level 3 …).
- Supports **parallel approval** at the same level (all approvers at a level must act before
  progression).
- Supports a **WIP (Work-In-Progress) mode** where the document is staged until fully approved,
  then saved automatically.

### What it does NOT do (currently)

- It does **not auto-approve** or **auto-reject** when a task's SLA deadline is breached. Breach
  is tracked and visible but requires a manual action or a custom background job to trigger
  automatic resolution.
- It does **not send notifications by itself** — notification delivery is handled by the
  Action Processor (email/SMS/webhook) which is a separate system that listens to workflow events.

---

## 2. Key Concepts

| Term | Meaning |
|------|---------|
| **Workflow Definition** | The template describing the steps, levels, and rules for a type of document |
| **Workflow Step** | A single approval node within the definition — one person (or group) acts on it |
| **Approval Level** | A numbered stage (1, 2, 3…). Steps sharing the same level run in parallel |
| **Workflow Instance** | A live runtime copy of a definition, created when a specific document is submitted |
| **Workflow Task** | The to-do item assigned to an individual approver for a specific step |
| **Workflow Config** | A rule that says "for documents of type X in OU Y, use workflow definition Z" |
| **Facts** | A key-value dictionary of document field values used to evaluate conditions |
| **EvalCondition** | A boolean expression evaluated against Facts to decide applicability or step routing |
| **SLA** | Service Level Agreement — the number of hours an approver has before the task is overdue |
| **WIP** | A mode where the document is staged (not yet saved) until final approval, then committed |
| **Assignment Strategy** | The rule that determines which user(s) receive a task |
| **Particulars** | A template string (e.g. `##DocumentNumber## - ##PartyName##`) that generates a human-readable label for the task |

---

## 3. System Architecture

```
┌──────────────────────────────────────────────────────────────────────┐
│  FRONTEND / CLIENT                                                   │
│  • Calls CheckApplicability to show/hide "Submit for Approval"       │
│  • Calls StartWorkflow on user action                                │
│  • Calls WorkFlowActions (Approve / Reject / Return / Escalate)      │
│  • Polls GetWorkFlowApprovalList for inbox                           │
└──────────────────────┬───────────────────────────────────────────────┘
                       │ REST API (FastEndpoints)
┌──────────────────────▼───────────────────────────────────────────────┐
│  SERVICE LAYER (FrameworkSL)                                         │
│  Endpoints: StartWorkflow, CheckApplicability, WorkFlowActions,      │
│  GetWorkFlowApprovalList, WorkflowHistory, WorkflowStatus, …        │
└──────────────────────┬───────────────────────────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────────────────────────┐
│  BUSINESS LOGIC LAYER (FrameworkBLL)                                 │
│  WorkFlowBLL — orchestrates engine calls, dispatches WIP callbacks,  │
│  applies Particulars template substitution at read time              │
└──────────────────────┬───────────────────────────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────────────────────────┐
│  WORKFLOW ENGINE (GB5Shared)                                         │
│  WorkFlowEngine — core state machine                                 │
│  • CheckWorkFlowApplicability                                        │
│  • StartWorkflowAsync                                                │
│  • HandleActionsAsync                                                │
│  WorkflowConditionEvaluator — boolean expression evaluator           │
│  ParticularsFormatter — ##token## template substitution              │
└──────────────────────┬───────────────────────────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────────────────────────┐
│  RUNTIME / DATA ACCESS (WorkFlowRunTime + WorkFlowDAL)               │
│  Reads and writes all workflow database tables via IQueryExecutor     │
└──────────────────────┬───────────────────────────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────────────────────────┐
│  DATABASE (SQL Server / PostgreSQL)                                  │
│  Master: MWORKFLOW, MWORKFLOWDETAIL, MWORKFLOWASSIGNMENT,           │
│          MWORKFLOWCONFIG                                             │
│  Transaction: TWORKFLOWINSTANCE, TWORKFLOWTASK, TWORKFLOWHISTORY,   │
│               TWORKFLOWWIP                                           │
└──────────────────────────────────────────────────────────────────────┘
```

### Layer responsibilities

| Layer | Project | Responsibility |
|-------|---------|----------------|
| SL | FrameworkSL | HTTP endpoints, request/response mapping |
| BLL | FrameworkBLL | Orchestration, WIP dispatch, Particulars enrichment |
| Engine | GB5Shared | Core state machine — applicability, start, actions |
| Runtime | GB5Shared | DB reads/writes for all workflow tables |
| DAL | FrameworkDAL | Transaction wrappers around engine calls |

---

## 4. Database Schema

### Master Tables (configuration / design time)

#### MWORKFLOW — Workflow Definitions

| Column | Type | Description |
|--------|------|-------------|
| WORKFLOWID | INT | Primary key |
| WORKFLOWNAME | NVARCHAR | Human-readable name |
| ENTITYID | INT | The entity type this workflow belongs to (FK → MENTITY) |
| PARTICULARS | NVARCHAR | Free-text description of the workflow |
| TENANTID | INT | Tenant isolation |
| VERSION, STATUS, SORTORDER | Standard fields | Standard audit/lifecycle fields |
| CREATEDBYID, CREATEDON, MODIFIEDBYID, MODIFIEDON | Standard fields | Audit trail |

#### MWORKFLOWDETAIL — Workflow Steps

| Column | Type | Description |
|--------|------|-------------|
| WORKFLOWDETAILID | INT | Primary key |
| WORKFLOWID | INT | Parent workflow (FK → MWORKFLOW) |
| SLNO | INT | Sequence number within the workflow |
| APPROVALLEVEL | INT | Approval level — steps sharing the same value run in parallel |
| STEPKEY | NVARCHAR | Unique string key (used in history records) |
| DISPLAYNAME | NVARCHAR | Label shown to approvers |
| STEPTYPE | INT | 0=Human, 1=System, 2=Auto, 3=End, 4=ParallelGroup |
| ISINITIAL | BIT | 1 = entry point step |
| ISFINAL | BIT | 1 = terminal step |
| ASSIGNMENTTYPE | INT | 0=User, 1=Role, 2=Pool, 3=Dynamic |
| EVALCONDITION | NVARCHAR | Boolean expression; if null/empty = always active |
| ASSIGNMENTID | INT | FK → MWORKFLOWASSIGNMENT |
| PARTICULARS | NVARCHAR | Step-level description (informational) |
| PARALLELGROUPKEY | NVARCHAR | Groups steps that run in parallel |
| SLAHOURS | INT | SLA hours for this step (0 = no SLA) |
| ESCALATIONRULEGROUPID | INT | FK to rule group defining escalation behaviour |
| PICKLISTID | INT | For form-based steps |
| DISPLAYLABELID | INT | For form-based steps |

#### MWORKFLOWASSIGNMENT — Assignment Rules

| Column | Type | Description |
|--------|------|-------------|
| WORKFLOWASSIGNMENTID | INT | Primary key |
| WORKFLOWASSIGNMENTNAME | NVARCHAR | Human-readable label |
| STRATEGYTYPE | TINYINT | 0=User, 1=UserGroup, 2=EnrichQualifier, 3=DynamicSQL, 4=Service |
| RULEEXPRESSION | NVARCHAR | Content depends on strategy type (see §9) |
| ASSIGNMENTDESCRIPTION | NVARCHAR | Free text description |
| ASSIGNMENTTENANTID | INT | Tenant isolation |

#### MWORKFLOWCONFIG — Activation Configuration

| Column | Type | Description |
|--------|------|-------------|
| WORKFLOWCONFIGID | INT | Primary key |
| CLIENTID | INT | Tenant (-1 = any) |
| ENTITYID | INT | Entity type (-1 = any) |
| OUID | INT | Organisational unit (-1 = any) |
| BIZTRANSACTIONCLASSID | INT | Transaction class (-1 = any) |
| ISBIZTRANSACTIONWISE | BIT | 0 = ignore BIZTRANSACTIONID, 1 = match it |
| BIZTRANSACTIONID | INT | Specific transaction type (-1 = any) |
| ISFORMBASEDAPPROVAL | BIT | 0 = regular mode, 1 = WIP mode |
| WORKFLOWID | INT | Which workflow to use (FK → MWORKFLOW) |
| EVALCONDITION | NVARCHAR | Additional condition against facts (e.g. `Amount >= 50000`) |
| REMARKS | NVARCHAR | Admin notes |
| CALLBACKENDPOINT | NVARCHAR | WIP mode only — URL to call after final approval |
| PARTICULARSTEMPLATE | NVARCHAR(500) | Template for task display text (e.g. `##DocumentNumber## - ##PartyName##`) |

### Transaction Tables (runtime state)

#### TWORKFLOWINSTANCE — Live Approval Instances

| Column | Type | Description |
|--------|------|-------------|
| WORKFLOWINSTANCEID | INT | Primary key |
| TENANTID | INT | Tenant |
| ENTITYID | INT | Entity type |
| OBJECTID | INT | PK of the document being approved |
| OUID | INT | OU of submission |
| BIZTRANSACTIONTYPEID | INT | Transaction type at submission time |
| WORKFLOWID | INT | Definition used |
| DATAJSON | NVARCHAR(MAX) | Snapshot of document DTO at submission time |
| FACTSJSON | NVARCHAR(MAX) | Enriched qualifier facts persisted for multi-level evaluation |
| CURRENTSTEPID | BIGINT | Current active step |
| CURRENTAPPROVALLEVEL | INT | Current approval level (1-based) |
| WORKFLOWSTATUS | TINYINT | 0=Pending, 1=Completed, 2=Rejected, 3=Returned |

#### TWORKFLOWTASK — Per-Approver Tasks

| Column | Type | Description |
|--------|------|-------------|
| WORKFLOWTASKID | INT | Primary key |
| WORKFLOWINSTANCEID | INT | Parent instance |
| STEPID | INT | The step this task belongs to |
| WORKFLOWTASKSTATUS | TINYINT | 0=Pending, 1=Approved, 2=Rejected, 3=Completed |
| ASSIGNEDTOUSERID | INT | User who received this task |
| ASSIGNEDROLEID | INT | Role assigned (nullable) |
| ASSIGNEDUSERGROUPID | INT | User group assigned (nullable) |
| DUEON | DATETIME | SLA deadline (null if no SLA on step) |
| COMPLETEDON | DATETIME | When the approver acted |
| ACTIONTAKEN | INT | The action code taken (nullable until acted) |
| COMMENT | NVARCHAR | Approver comment |

#### TWORKFLOWHISTORY — Immutable Audit Trail

| Column | Type | Description |
|--------|------|-------------|
| WORKFLOWHISTORYID | INT | Primary key (identity) |
| WORKFLOWINSTANCEID | INT | Parent instance |
| ENTITYID, OBJECTID | INT | Document reference |
| DATAJSON | NVARCHAR(MAX) | Document snapshot at time of action |
| WORKFLOWID | INT | Workflow definition used |
| STEPKEY | NVARCHAR | Step identifier |
| APPROVALLEVEL | INT | Level at which action occurred |
| ACTION | TINYINT | 0=Submitted, 1=Approved, 2=Rejected, 3=Returned, 4=Escalated |
| ACTIONBYUSERID | INT | User who acted |
| COMMENT | NVARCHAR | Comment left by user |
| ACTIONON | DATETIME | Exact timestamp |

#### TWORKFLOWWIP — WIP Staging (Form-Based Approval)

| Column | Type | Description |
|--------|------|-------------|
| WIPID | INT | Identity PK |
| ENTITYID, OBJECTID | INT | Document reference |
| TENANTID | INT | Tenant |
| WORKFLOWINSTANCEID | INT | Linked instance (set after creation) |
| DATAJSON | NVARCHAR(MAX) | Full document DTO staging area |
| DATAHASH | VARBINARY(32) | Hash for change detection |
| RESUBMISSIONCOUNT | SMALLINT | How many times re-submitted after return |
| LASTACTION | TINYINT | Last action taken (0=Submit, 1=Approve, 2=Reject, 3=Return) |
| STATUS | TINYINT | 0=Cancelled, 1=Pending, 2=Approved, 3=Rejected, 4=Returned, 5=Withdrawn |
| APIENDPOINT | NVARCHAR | Callback URL for final approval dispatch |

---

## 5. Workflow Definition — Setup

A **workflow definition** (stored in MWORKFLOW + MWORKFLOWDETAIL) describes the approval
process template. It is entity-specific and reusable across many documents.

### Step 1 — Create the workflow header

Use `POST /Workflow/SaveWorkflow` with a `WorkflowDTO` containing:

| Field | Description |
|-------|-------------|
| `WorkflowName` | Descriptive name (e.g. "Purchase Order 2-Level Approval") |
| `EntityId` | The entity type (e.g. the EntityId for "Task" or "PurchaseOrder") |

### Step 2 — Define the steps

Each step maps to one row in MWORKFLOWDETAIL. Key fields to set:

| Field | Description | Example |
|-------|-------------|---------|
| `ApprovalLevel` | Stage number. Steps with the same level run in parallel. | 1, 2, 3 |
| `DisplayName` | Label shown to approvers in the inbox | "Line Manager Approval" |
| `StepType` | Type of step. Set to 0 (Human) for manual approvals | 0 |
| `AssignmentId` | FK to MWORKFLOWASSIGNMENT — determines who gets the task | 42 |
| `SlaHours` | Hours before DueOn is marked overdue. 0 = no deadline | 48 |
| `EvalCondition` | Optional condition to skip this step. If blank, always active | `Amount < 10000` |
| `StepKey` | Unique string identifier for this step across the workflow | "LEVEL1_MANAGER" |

### Step 3 — Create assignment rules

Each step's `AssignmentId` points to an MWORKFLOWASSIGNMENT row. Set `StrategyType` and
`RuleExpression` according to §9.

### Multi-level example

A three-level purchase order approval:

| Level | DisplayName | StrategyType | RuleExpression | SlaHours |
|-------|-------------|--------------|----------------|----------|
| 1 | Line Manager Approval | 2 (EnrichQualifier) | `LineManagerId` | 48 |
| 2 | Finance Approval | 0 (User) | `15` (userId=15) | 24 |
| 3 | Director Approval | 1 (UserGroup) | `8` (groupId=8) | 72 |

Level 1 and Level 2 are sequential. Both must be completed before the document reaches Level 3.

### Parallel approval example

To require TWO approvers at Level 2 simultaneously:

| Level | DisplayName | AssignmentId |
|-------|-------------|--------------|
| 1 | Line Manager | A |
| 2 | Finance Manager | B |
| 2 | Compliance Officer | C |
| 3 | Director | D |

Both Finance Manager and Compliance Officer must act before Level 3 begins.

---

## 6. Workflow Configuration — Activation Rules

A workflow definition by itself does nothing. It must be activated via **MWORKFLOWCONFIG**,
which maps a dimension combination (entity + OU + transaction type) to a workflow definition,
optionally with a condition.

### Dimensions (all support -1 = "any")

| Dimension | Meaning |
|-----------|---------|
| `CLIENTID` | Tenant. Use -1 for system-wide rules. |
| `ENTITYID` | Entity type (e.g. Task entity). |
| `OUID` | Organisational Unit. Use -1 to apply across all OUs. |
| `BIZTRANSACTIONCLASSID` | Transaction class. Use -1 to ignore class. |
| `ISBIZTRANSACTIONWISE` | Set 1 to also match `BIZTRANSACTIONID`. |
| `BIZTRANSACTIONID` | Specific transaction type within the class. |

### How specificity works

The engine queries all matching config rows and evaluates them **most-specific-first**. The
first row whose `EVALCONDITION` passes against the document's facts wins.

Specificity ordering: rows with more non-(-1) dimensions rank higher.

**Example:** Two config rows for the Task entity:

| Row | OuId | EvalCondition | WorkflowId |
|-----|------|---------------|-----------|
| A | -1 (any) | `TaskDetailType == 0` | 10 (simple 1-level) |
| B | 200 (OU=HQ) | `TaskDetailType == 0` | 20 (3-level for HQ) |

A Task submitted from OU=HQ with TaskDetailType=0 → Row B wins (more specific). From any
other OU → Row A wins.

### EvalCondition on config rows

This condition is evaluated against the **Facts** dictionary passed at applicability check
time. It uses the same expression syntax as step conditions (see §10).

Examples:
- `Amount >= 50000` — only activate workflow for high-value documents
- `TaskDetailType == 1 || TaskDetailType == 2` — specific task types only
- *(empty)* — always activate

### Particulars template

`PARTICULARSTEMPLATE` on the config row defines the display text shown in approver inboxes.
Use `##FieldName##` tokens where `FieldName` matches a property name in the document's DTO.

Example: `##DocumentNumber## | ##PartyName## | ##DocumentDate##`

At read time, the engine replaces tokens from the `DataJson` snapshot. Template changes
take effect immediately on all in-flight approval tasks without any data migration.

---

## 7. How a Document Enters Workflow

This section explains the exact sequence from the moment a user saves a document to the
moment an approver sees it in their inbox.

```
User saves document
        │
        ▼
EventHandler.ExecuteSaveAsync()
  ├── Validate DTO
  ├── Persist document to DB (via DAL callback)
  ├── Call CheckWorkFlowApplicability(WorkflowCheckContext, facts)
  │       └── Engine queries MWORKFLOWCONFIG, evaluates EvalCondition
  │           Returns: { IsEnabled, WorkFlowMode, WorkFlowId, IsFormBased }
  │
  ├── [If IsEnabled=false] → Normal save, no workflow
  │
  └── [If IsEnabled=true]
          │
          ├── [Regular mode] → StartWorkflow(WorkflowStartRequest)
          │       └── Creates TWORKFLOWINSTANCE + TWORKFLOWTASK(s) at Level 1
          │
          └── [WIP mode] → StartWorkflow → inserts TWORKFLOWWIP first, then instance
```

### Facts dictionary

Facts are a snapshot of key field values from the document, passed explicitly by the module
BLL when calling `ExecuteSaveAsync`. They are used for:

1. Evaluating `MWORKFLOWCONFIG.EVALCONDITION` (which workflow applies)
2. Evaluating `MWORKFLOWDETAIL.EVALCONDITION` (which steps are active at each level)
3. Resolving EnrichQualifier assignment strategies (see §9)

The Facts dictionary is persisted in `TWORKFLOWINSTANCE.FACTSJSON` so that Level 2+
evaluations use the same enriched base facts as Level 1.

---

## 8. The Approval Lifecycle

### States

```
Document submitted
        │
        ▼
  WORKFLOWSTATUS = 0 (Pending)
  WORKFLOWTASKSTATUS = 0 (Pending) for all Level 1 tasks
        │
  Approver acts
        │
  ┌─────┴──────────────────────────────────┐
  │                                        │
  ▼                                        ▼
Approve (action=1)                    Reject (action=2)
  │                                        │
  ▼                                        ▼
All Level N tasks complete?      WORKFLOWSTATUS = 2 (Rejected)
  │                               Instance terminates
  ├─ No → Wait for other
  │         parallel approvers
  │
  └─ Yes → GetNextApprovalLevel()
                │
                ├─ Level N+1 exists → Create tasks at N+1, continue
                │
                └─ No next level → WORKFLOWSTATUS = 1 (Completed) ✓
```

### Return action

Return (action=3) sends the document back one level. If at Level 1, it returns to the
submitter (Level 0). The submitter can then resubmit. Each resubmission increments the
`RESUBMISSIONCOUNT` on WIP records.

### Escalate action

Escalate (action=4) is recorded in `TWORKFLOWHISTORY`. The escalation rule group
(`MWORKFLOWDETAIL.ESCALATIONRULEGROUPID`) defines the intended escalation target, but
**automatic execution requires a custom background job** — there is no built-in scheduler
that triggers escalation automatically on SLA breach.

### Completion

When the last approval level is fully approved:
- `TWORKFLOWINSTANCE.WORKFLOWSTATUS` is set to 1 (Completed)
- All remaining `TWORKFLOWTASK` rows are marked Completed
- In WIP mode: `TWORKFLOWWIP.STATUS` is set to 2 (Approved), and the callback endpoint
  is called with the staged `DataJson`, which commits the document to the database

---

## 9. Assignment Strategies — Who Gets the Task?

Each workflow step has an `MWORKFLOWASSIGNMENT` row that determines who receives the task.
The strategy is set via `STRATEGYTYPE`.

### Strategy 0 — User (static)

`RuleExpression` = a single integer UserId.

The task is always assigned to this specific user.

**Use case:** Fixed approver (e.g. "always goes to the Finance Director").

```
STRATEGYTYPE = 0
RULEEXPRESSION = "15"   ← UserId = 15
```

### Strategy 1 — UserGroup (static group)

`RuleExpression` = a single integer UserGroupId.

The task is assigned to the group. All members of the group see the task; the first to act
claims it.

**Use case:** Pool of approvers (e.g. "any member of the Accounts team can approve").

```
STRATEGYTYPE = 1
RULEEXPRESSION = "8"   ← UserGroupId = 8
```

### Strategy 2 — EnrichQualifier (dynamic field lookup)

`RuleExpression` = a property name from the Facts dictionary.

The actual UserId is resolved at runtime from the Facts. The module populates Facts with
values like `{"LineManagerId": 42, "DepartmentHeadId": 99}`.

**Use case:** Hierarchical approval where the approver depends on the document's owner or
organisational context.

```
STRATEGYTYPE = 2
RULEEXPRESSION = "LineManagerId"   ← Facts["LineManagerId"] = 42 → task assigned to user 42
```

Facts are persisted in `TWORKFLOWINSTANCE.FACTSJSON` so that Level 2 and Level 3 steps can
resolve their assignees from the same enriched data that was available at Level 1.

### Strategy 3 — DynamicSQL (query-based)

`RuleExpression` = a parameterised SQL query.

The engine executes the query at runtime, injecting fact values as parameters. The query must
return a UserId.

**Use case:** Complex assignment logic that requires a database lookup (e.g. "find the head
of the cost centre this document belongs to").

### Strategy 4 — Service (reserved)

Reserved for future service-based assignment resolution.

---

## 10. Condition Evaluation Engine

Both `MWORKFLOWCONFIG.EVALCONDITION` and `MWORKFLOWDETAIL.EVALCONDITION` use a built-in
boolean expression evaluator (`WorkflowConditionEvaluator`).

### Syntax

Conditions are C#-like boolean expressions evaluated against the Facts dictionary.

#### Comparison operators

| Operator | Meaning |
|----------|---------|
| `==` | Equal |
| `!=` | Not equal |
| `>` | Greater than |
| `<` | Less than |
| `>=` | Greater than or equal |
| `<=` | Less than or equal |

#### Logical operators

| Operator | Alias | Meaning |
|----------|-------|---------|
| `&&` | `AND` | Both conditions must be true |
| `\|\|` | `OR` | Either condition must be true |
| `!` | `NOT` | Negate |

#### Grouping

Parentheses `( )` control evaluation order.

#### Type coercion

The evaluator auto-coerces types for comparison:
- If both sides parse as numbers → numeric comparison
- If both sides parse as booleans → boolean comparison
- If both sides parse as dates → date comparison
- Otherwise → case-insensitive string comparison
- `null == null` → true

#### Examples

```
TaskDetailType == 0
TaskDetailType == 0 || TaskDetailType == 1 || TaskDetailType == 2
Amount >= 50000 && CategoryId != 5
!(StatusId == 3)
DepartmentId == 10 && Amount > 10000
```

A **null or empty** condition is equivalent to `true` — the step or config always applies.

### How facts are populated

Facts are supplied at two points:

1. **At workflow start** — the module BLL passes them in `WorkflowStartRequest.Facts`
   (e.g. `{"TaskDetailType": 2, "Amount": 85000, "LineManagerId": 42}`)
2. **At each action** — `WorkFlowActionItemDTO.Facts` can augment or override facts for
   the next level's condition evaluation

Facts from Level 1 are stored in `TWORKFLOWINSTANCE.FACTSJSON`. At every subsequent level,
the engine merges:
- Base: persisted `FACTSJSON` (from Level 1 start)
- Override: `DataJson` DTO field values
- Highest priority: new facts from the current action context

---

## 11. WIP (Form-Based Approval) Mode

WIP mode is used when the document should **not be permanently saved** until the entire
approval chain is complete. The document is staged in `TWORKFLOWWIP` and only committed
to the real entity tables when the final approver approves.

### When to use WIP

Use WIP mode when:
- A new record must go through approval before being created
- Changes to an existing record must be reviewed before taking effect
- The workflow may involve Return actions that modify the document before resubmission

### WIP flow

```
1. User fills form and clicks "Submit for Approval"
2. Frontend calls POST /WorkFlow/StartWorkflow
3. Engine inserts TWORKFLOWWIP (STATUS=1 Pending, DataJson=full DTO)
4. Engine creates TWORKFLOWINSTANCE (ObjectId=WipId initially)
5. Normal multi-level approval proceeds

--- At final approval ---

6. Engine sets TWORKFLOWWIP.STATUS=2 (Approved)
7. Engine returns WipDispatchInfo to BLL
8. BLL calls WipApprovalDispatcher.DispatchAsync() AFTER transaction commits
9. Dispatcher sends POST to MWORKFLOWCONFIG.CALLBACKENDPOINT with DataJson
10. Target service receives the approved DTO and saves it normally
```

### Configuration

| MWORKFLOWCONFIG field | Value for WIP |
|-----------------------|---------------|
| `ISFORMBASEDAPPROVAL` | 1 |
| `CALLBACKENDPOINT` | Full URL of the entity save endpoint (e.g. `http://task-service:5001/Task/SaveTask`) |

### WIP status codes

| Value | Meaning |
|-------|---------|
| 0 | Cancelled |
| 1 | Pending (in approval) |
| 2 | Approved (final approval given, callback dispatched) |
| 3 | Rejected |
| 4 | Returned (sent back to submitter) |
| 5 | Withdrawn |

### Resubmission

When a WIP document is returned, the submitter can edit and resubmit. Each cycle increments
`TWORKFLOWWIP.RESUBMISSIONCOUNT`. The `DataJson` in the WIP row is updated with the revised
document, and the workflow restarts from Level 1.

---

## 12. SLA, Due Dates, and Escalation

### SLA configuration

Each step can have an SLA defined in hours via `MWORKFLOWDETAIL.SLAHOURS`.

When the engine creates a task, if `SlaHours > 0`, it calculates:
```
TWORKFLOWTASK.DUEON = creation time + SlaHours
```

If `SlaHours = 0`, `DUEON` is null (no deadline).

### What happens on SLA breach

**Currently: nothing automatic.**

The `DUEON` field is returned in the approval list API and visible to approvers. It is the
application's (or a custom background job's) responsibility to take action when a deadline
is breached.

The `WorkflowAction.Escalate = 4` action exists and is recorded in history, but there is no
built-in scheduler that triggers it automatically. To implement automatic escalation:

1. Create a background service (`IHostedService`) that periodically queries:
   `SELECT * FROM TWORKFLOWTASK WHERE DUEON < GETDATE() AND WORKFLOWTASKSTATUS = 0`
2. For each overdue task, call `WorkFlowActions` with `Action = 4` (Escalate)
3. The escalation target is defined by `MWORKFLOWDETAIL.ESCALATIONRULEGROUPID`

### Visual indication

The approval list endpoint returns `DueOn` on every task row. The frontend should use this
to:
- Highlight overdue tasks (DueOn < now)
- Show tasks approaching deadline (DueOn < now + warning threshold)

---

## 13. User Delegation

When a user is unavailable (e.g. on leave), they can delegate their approval authority to
another user. The engine respects active delegations transparently.

### How it works

Before creating a task, the engine calls `ResolveEffectiveUserAsync(userId, now)`. If an
active delegation record exists for that user, the task is assigned to the delegate instead.

Delegations are stored in a `UserDelegation` record with `StartsOn` and `EndsOn` timestamps.
A delegation is active when `now` falls within the `[StartsOn, EndsOn]` range.

---

## 14. Particulars — Display Text

Approvers need a human-readable description of what they are approving. Without it, the inbox
would show only entity codes and IDs.

### How it works

1. Admin sets `MWORKFLOWCONFIG.PARTICULARSTEMPLATE` for a config row, e.g.:
   ```
   ##DocumentNumber## | ##PartyName## | ##DocumentDate##
   ```
2. When `GET /WorkFlow/GetWorkFlowApprovalList` is called, the BLL:
   - Fetches the template from the config row (via SQL JOIN — no extra round-trip)
   - For each task in the result, calls `ParticularsFormatter.Apply(template, DataJson)`
   - Substitutes `##FieldName##` tokens with matching property values from the stored `DataJson`
3. The populated `Particulars` string is returned on every `WorkflowApprovalListDTO` row

### Template syntax

- Token: `##PropertyName##` where `PropertyName` is a case-insensitive match against a
  JSON property in the document DTO snapshot.
- Unmatched tokens remain as-is.
- A null/empty template → `Particulars` is null on the response.

### Examples

| Template | Result |
|----------|--------|
| `##DocumentNumber## - ##PartyName##` | `PO-2024-0042 - Acme Supplies Ltd` |
| `##TaskCode## \| ##AssignedTo## \| ##DueDate##` | `TASK-001 \| John Smith \| 2024-06-30` |
| `##Amount## approval from ##DepartmentName##` | `85000.00 approval from Procurement` |

### Template is admin-configurable

Because the template is stored in `MWORKFLOWCONFIG` and applied at read time, the admin can
update the format at any time. The change is reflected immediately for all existing in-flight
tasks without any data migration.

---

## 15. Audit Trail and History

Every action taken during a workflow is permanently recorded in `TWORKFLOWHISTORY`. This
table is append-only and is never modified after insertion.

### What is recorded

| Event | ACTION value | When |
|-------|-------------|------|
| Document submitted | 0 | When StartWorkflow is called |
| Task approved | 1 | When approver takes Approve action |
| Task rejected | 2 | When approver takes Reject action |
| Task returned | 3 | When approver takes Return action |
| Task escalated | 4 | When approver or system takes Escalate action |

Each history row contains:
- The action type and timestamp
- The user who acted
- The comment they left
- The approval level
- A snapshot of the document's `DataJson` at that moment (for auditability)
- The step key identifying which step was acted upon

### Accessing history

`POST /WorkFlow/WorkflowHistory` — returns `List<WorkflowHistoryListDTO>`

Filters available: `userId`, `ouId`, `entityId`, `fromDate`, `toDate`.

---

## 16. API Reference

### Workflow Engine Endpoints

#### POST /WorkFlow/CheckApplicability

Checks whether a workflow applies for a given document context **without starting** one.
Use this to decide whether to show a "Submit for Approval" button.

**Request body:** `WorkflowCheckContext`
```json
{
  "EntityId": 42,
  "OUId": 200,
  "BizTransactionClassId": -1,
  "BizTransactionId": -1,
  "Facts": { "TaskDetailType": 2, "Amount": 85000 }
}
```

**Response:** `WorkflowPreCheckResult`
```json
{
  "IsEnabled": true,
  "WorkFlowMode": 1,
  "WorkFlowId": 10,
  "WorkflowConfigId": 3,
  "IsFormBasedApproval": false,
  "CallbackEndpoint": null
}
```

`WorkFlowMode`: 0=None, 1=Regular, 2=WIP

---

#### POST /WorkFlow/StartWorkflow

Starts an approval workflow for a document.

**Request body:** `WorkflowStartRequest`
```json
{
  "EntityId": 42,
  "OUId": 200,
  "BizTransactionClassId": -1,
  "BizTransactionId": -1,
  "ObjectId": 1001,
  "DataJson": { ...full document DTO... },
  "Comment": "Submitted for approval",
  "Facts": { "TaskDetailType": 2, "Amount": 85000, "LineManagerId": 55 }
}
```

**Response:** Success message or `"No workflow configured for this entity."`

---

#### POST /WorkFlow/WorkFlowActions

Submits one or more approval actions (approve, reject, return, escalate).

**Request body:** `WorkFlowActionDTO`
```json
{
  "Items": [
    {
      "TaskId": 301,
      "Action": 1,
      "Comment": "Looks good, approved.",
      "Facts": {}
    }
  ]
}
```

Action codes: 0=Submit, 1=Approve, 2=Reject, 3=Return, 4=Escalate

---

#### POST /WorkFlow/GetWorkFlowApprovalList

Returns the approval inbox for the logged-in user.

**Query parameter:** `WorkflowType` — 1 = "Tasks assigned to me", other = "Tasks I submitted"

**Request body:** `CriteriaDTO`
```json
{
  "EntityId": null,
  "Status": null,
  "FromDate": null,
  "ToDate": null
}
```

**Response:** `List<WorkflowApprovalListDTO>` — includes `Particulars` field populated from template.

---

#### GET /WorkFlow/CurrentApprovalStatus?EntityId=42&ObjectId=1001

Returns the current approval status for a specific document. Useful for showing "pending at
Level 2 — Finance Manager" on the document form.

---

#### POST /WorkFlow/WorkflowHistory

Returns the full approval audit trail.

---

#### POST /WorkFlow/WorkflowStatus

Returns approval status summary for all instances the user is involved in.

---

### Workflow Config Admin Endpoints

#### GET /WorkflowConfig/GetWorkflowConfig?WorkflowConfigId=3

Retrieves a single workflow config row.

---

#### POST /WorkflowConfig/SaveWorkflowConfig

Creates or updates a workflow config row.

**Request body:** `WorkflowConfigDTO`
```json
{
  "WorkflowConfigId": 0,
  "WorkflowConfigClientId": -1,
  "WorkflowConfigEntityId": 42,
  "WorkflowConfigOuId": -1,
  "WorkflowConfigBizTransactionClassId": -1,
  "WorkflowConfigIsBizTransactionWise": false,
  "WorkflowConfigBizTransactionId": -1,
  "WorkflowConfigIsFormBasedApproval": false,
  "WorkflowConfigWorkflowId": 10,
  "WorkflowConfigEvalCondition": "Amount >= 50000",
  "WorkflowConfigRemarks": "High-value PO workflow",
  "WorkflowConfigParticularsTemplate": "##DocumentNumber## | ##PartyName## | ##Amount##"
}
```

---

#### DELETE /WorkflowConfig/DeleteWorkflowConfig?WorkflowConfigId=3

Deletes a workflow config row.

---

### Workflow Definition Admin Endpoints

#### GET /Workflow/GetWorkflow?WorkflowId=10

Retrieves a workflow definition with all its steps.

---

#### POST /Workflow/SaveWorkflow

Creates or updates a workflow definition.

---

#### DELETE /Workflow/DeleteWorkflow?WorkflowId=10

Deletes a workflow definition and all its steps.

---

## 17. Admin Setup Guide — Step by Step

This section walks an administrator through the end-to-end setup required to activate
approval workflows for a new document type.

### Prerequisites

- Know the `EntityId` for the document type (query MENTITY)
- Know the UserIds or UserGroupIds of intended approvers
- Know the `BizTransactionTypeId` if transaction-specific routing is needed

---

### Step 1 — Create assignment rules

For each distinct approver target, insert a row into MWORKFLOWASSIGNMENT.

**Example: Line Manager (dynamic, from document context)**
```
STRATEGYTYPE = 2
RULEEXPRESSION = "LineManagerId"
WORKFLOWASSIGNMENTNAME = "Document Owner's Line Manager"
```

**Example: Fixed Finance User**
```
STRATEGYTYPE = 0
RULEEXPRESSION = "15"
WORKFLOWASSIGNMENTNAME = "Finance Manager (User 15)"
```

---

### Step 2 — Create the workflow definition

Call `POST /Workflow/SaveWorkflow`:

```json
{
  "WorkflowId": 0,
  "WorkflowName": "Purchase Order Approval — 2 Level",
  "EntityId": 55,
  "WorkflowDetailArray": [
    {
      "SlNo": 1,
      "ApprovalLevel": 1,
      "StepKey": "PO_L1_MANAGER",
      "DisplayName": "Line Manager Approval",
      "StepType": 0,
      "AssignmentId": 21,
      "SlaHours": 48,
      "EvalCondition": ""
    },
    {
      "SlNo": 2,
      "ApprovalLevel": 2,
      "StepKey": "PO_L2_FINANCE",
      "DisplayName": "Finance Approval",
      "StepType": 0,
      "AssignmentId": 22,
      "SlaHours": 24,
      "EvalCondition": "Amount >= 10000"
    }
  ]
}
```

Note: Level 2 here has an `EvalCondition` — it is only activated for POs ≥ 10,000.
POs below 10,000 complete after Level 1.

---

### Step 3 — Create the workflow config

Call `POST /WorkflowConfig/SaveWorkflowConfig`:

```json
{
  "WorkflowConfigId": 0,
  "WorkflowConfigClientId": -1,
  "WorkflowConfigEntityId": 55,
  "WorkflowConfigOuId": -1,
  "WorkflowConfigBizTransactionClassId": -1,
  "WorkflowConfigIsBizTransactionWise": false,
  "WorkflowConfigBizTransactionId": -1,
  "WorkflowConfigIsFormBasedApproval": false,
  "WorkflowConfigWorkflowId": 10,
  "WorkflowConfigEvalCondition": "",
  "WorkflowConfigRemarks": "All POs require approval",
  "WorkflowConfigParticularsTemplate": "##DocumentNumber## | ##VendorName## | ##Amount##"
}
```

This activates the workflow for all Purchase Orders across all tenants and OUs.

---

### Step 4 — Verify with CheckApplicability

Before going live, test the configuration:

```
POST /WorkFlow/CheckApplicability
Body: { "EntityId": 55, "OUId": 100, "Facts": {"Amount": 75000} }
```

Expected response: `IsEnabled: true, WorkFlowId: 10`

---

### Step 5 (WIP mode only) — Set the callback endpoint

If using WIP mode, set `MWORKFLOWCONFIG.CALLBACKENDPOINT` to the full URL of the entity
save endpoint on the responsible microservice.

Example: `http://procurement-service:5003/PurchaseOrder/SavePurchaseOrder`

The dispatcher will POST the approved `DataJson` to this URL after final approval.

---

## 18. Developer Integration Guide

This section explains what a module developer must do to integrate a new document type
with the workflow engine.

### What BaseEntityAppService does automatically

When a module BLL calls `ExecuteSaveAsync`, the framework:

1. Checks workflow applicability via `CheckWorkFlowApplicability`
2. If applicable, calls `StartWorkflowAsync`
3. Passes `DataJson` (the DTO) and `Facts` to the engine

**No manual workflow calls are needed in module BLL code** unless the module needs
pre-flight information (e.g. to show or hide a "Submit" button).

### Passing Facts correctly

Facts are the key-value dictionary the engine uses to evaluate conditions and resolve
assignees. The module BLL must build this dictionary before calling `ExecuteSaveAsync`.

Required entries:
- Any field referenced in `MWORKFLOWCONFIG.EVALCONDITION` for this entity
- Any field referenced in `MWORKFLOWDETAIL.EVALCONDITION` for this entity's steps
- Any field name used as `RULEEXPRESSION` for EnrichQualifier assignments (StrategyType=2)

Example for a Task module:
```csharp
var facts = new Dictionary<string, object>
{
    ["TaskDetailType"] = dto.TaskDetailTypeId,
    ["Amount"]         = dto.TaskBudget,
    ["LineManagerId"]  = dto.AssignedManagerId,
    ["OUId"]           = loginDTO.WorkOUId
};
```

### Showing approval status on the document form

Use `GET /WorkFlow/CurrentApprovalStatus?EntityId=X&ObjectId=Y` to show the current
approval state on the document's edit form. This returns:
- Current approval level and step name
- Current assignee
- Whether the document is pending / completed / rejected

### Pre-flight check (optional)

If the UI needs to decide whether to show a "Submit for Approval" button before the user
saves the document, call `POST /WorkFlow/CheckApplicability` with the document's context.

---

## 19. Troubleshooting

### "No workflow configured for this entity"

The engine found no matching row in `MWORKFLOWCONFIG` for the given EntityId + OUId +
BizTransactionTypeId + Facts combination. Check:
1. Does an MWORKFLOWCONFIG row exist for this EntityId?
2. Is the `EVALCONDITION` actually satisfied by the Facts being passed?
3. Are the dimension fields (CLIENTID, OUID, etc.) set correctly (-1 for wildcards)?

### Task not assigned to expected approver

For `StrategyType=2` (EnrichQualifier), verify:
1. The `RULEEXPRESSION` exactly matches a key in the Facts dictionary (case-sensitive)
2. The Facts value is a valid integer UserId
3. The Facts were correctly populated in the module's `ExecuteSaveAsync` call

### Level 2 never starts after Level 1 approval

Check that `MWORKFLOWDETAIL.EVALCONDITION` on the Level 2 step is satisfied by the Facts.
An always-active Level 2 step should have an empty/null `EVALCONDITION`.

### WIP callback not firing

Verify:
1. `MWORKFLOWCONFIG.CALLBACKENDPOINT` is set to the correct full URL
2. The target service is reachable from the workflow service at that URL
3. Check logs for `WipApprovalDispatcher` — dispatch errors are logged with the WIP ID

### Particulars field is null

Verify that `MWORKFLOWCONFIG.PARTICULARSTEMPLATE` is set for the relevant config row and
that the token names (`##FieldName##`) match actual property names in the document DTO
(case-insensitive).

---

## 20. Reference: Enums and Constants

### WorkflowAction

| Value | Name | Meaning |
|-------|------|---------|
| 0 | Submit | Document submitted for approval |
| 1 | Approve | Approver approves |
| 2 | Reject | Approver rejects — terminates workflow |
| 3 | Return | Approver returns to previous level (or submitter) |
| 4 | Escalate | Task escalated |

### WorkflowInstanceStatus (WORKFLOWSTATUS)

| Value | Name |
|-------|------|
| 0 | Pending |
| 1 | Completed |
| 2 | Rejected |
| 3 | Returned |

### WorkflowTaskStatus (WORKFLOWTASKSTATUS)

| Value | Name |
|-------|------|
| 0 | Pending |
| 1 | Approved |
| 2 | Rejected |
| 3 | Completed |

### WorkflowWipStatus

| Value | Name |
|-------|------|
| 0 | Cancelled |
| 1 | Pending |
| 2 | Approved |
| 3 | Rejected |
| 4 | Returned |
| 5 | Withdrawn |

### AssignmentStrategyType

| Value | Name | RuleExpression content |
|-------|------|----------------------|
| 0 | User | Integer UserId |
| 1 | UserGroup | Integer UserGroupId |
| 2 | EnrichQualifier | Key name from Facts dictionary |
| 3 | DynamicSQL | Parameterised SQL query |
| 4 | Service | Reserved |

### WorkflowType (GetWorkFlowApprovalList parameter)

| Value | Meaning |
|-------|---------|
| 1 | Request-To-Me (tasks assigned to me) |
| other | Request-By-Me (tasks I submitted) |

---

*Document generated from GB5 codebase analysis — GB5Framework + GB5Shared workflow modules.*
