> For the complete documentation index, see [llms.txt](https://host2host.onibonje.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://host2host.onibonje.com/docs/40-workflow-architecture.md).

# Workflow Architecture (Maker–Checker & Multi-Approval)

## 1. Overview

Human approvals — **maker–checker (four-eyes)** and **multi-level sign-off** — are handled by the **`h2h-workflow`** library with **Camunda BPM** as the execution engine.

| Concern                                             | Owner                                                 |
| --------------------------------------------------- | ----------------------------------------------------- |
| Approval policy (who, how many levels, which roles) | **Database** — `approval_policy`, `approval_step_def` |
| Maker–checker enforcement                           | **`h2h-workflow`** — `ApprovalService`                |
| Task inbox, SLA, escalation                         | **Camunda** + Admin Publish Center                    |
| Config / ops business rules                         | Domain services (`ConfigVersionService`, ops APIs)    |

**Principle:** Transaction hot path stays in Camel. Workflows govern **configuration changes**, **operational actions**, and **held payments** — never block STP unless a routing rule explicitly sends a message to manual review.

***

## 2. Module Layout

```
h2h-workflow-api/          # Stable contracts — no Camunda dependency
├── ApprovalService.java
├── ApprovalPolicyResolver.java
├── dto/                   # ApprovalRequest, ApprovalStep, ApprovalAction
└── event/                 # APPROVAL_SUBMITTED, APPROVAL_COMPLETED, APPROVAL_REJECTED

h2h-workflow-camunda/      # Camunda 7/8 implementation
├── engine/
│   ├── CamundaApprovalService.java
│   ├── ProcessDeployer.java           # BPMN from approval_policy
│   └── MakerCheckerDelegate.java      # four-eyes guard
├── listener/
│   └── ApprovalCompletionListener.java
├── camel/
│   └── CamundaWorkflowComponent.java  # camunda:submit, camunda:resume
└── config/
    └── WorkflowAutoConfiguration.java
```

| Consumer module      | Uses                                               |
| -------------------- | -------------------------------------------------- |
| `h2h-config-runtime` | Submit config publish → `ApprovalService.submit()` |
| `h2h-admin-api`      | REST `/approvals/*`, Publish Center queue          |
| `h2h-camel-core`     | `camunda:manual-review` on routing-rule hold       |
| `h2h-job-scheduler`  | Optional approval before destructive SQL tasks     |

**Deployment:** Camunda runs as a **sidecar** or **shared cluster** (Helm sub-chart). `h2h-runtime` and `h2h-admin-api` both depend on `h2h-workflow-camunda`.

***

## 3. Maker–Checker (Four-Eyes)

### 3.1 Rules (enforced in code, not UI-only)

| Rule                           | Enforcement                                                                                               |
| ------------------------------ | --------------------------------------------------------------------------------------------------------- |
| **R1 — No self-approval**      | `submitted_by` ≠ `approved_by` on every step                                                              |
| **R2 — Maker cannot publish**  | Only roles with `config:publish` or `operations:approve` complete final step                              |
| **R3 — Segregation of duties** | Onboarding officer creates DRAFT; supervisor publishes — see [06 Personas](/docs/06-personas-and-rbac.md) |
| **R4 — Immutable audit**       | Every action → `approval_action` + `config_audit.approval_ref`                                            |
| **R5 — Scope match**           | Approver Keycloak scope must cover entity country/partner                                                 |

```java
public interface ApprovalService {

    ApprovalRequest submit(SubmitApprovalCommand cmd);

    ApprovalRequest approve(String requestId, ApproveCommand cmd);

    ApprovalRequest reject(String requestId, RejectCommand cmd);

    ApprovalRequest delegate(String requestId, DelegateCommand cmd);

    Optional<ApprovalRequest> findPending(String entityType, String entityId);
}
```

`MakerCheckerDelegate` (Camunda Java delegate) throws `ApprovalViolationException` if R1 or R5 fails before completing a user task.

### 3.2 Config publish maker–checker flow

```mermaid
sequenceDiagram
  participant Maker as Integration Analyst
  participant Admin as Admin API
  participant WF as h2h-workflow
  participant Camunda
  participant Checker as Integration Supervisor
  participant DB as Config Store
  participant Bus as Event Bus

  Maker->>Admin: Edit DRAFT + run test harness
  Maker->>Admin: POST /publish/submit
  Admin->>DB: status = PENDING_APPROVAL
  Admin->>WF: submit(CONFIG_PUBLISH, entityRef, makerId)
  WF->>Camunda: start process from approval_policy
  Camunda->>Checker: User task in Publish Center
  Note over Checker: Checker ≠ Maker (R1)
  Checker->>Admin: POST /approvals/{id}/approve
  Admin->>WF: approve(requestId, checkerId)
  WF->>Camunda: complete task
  Camunda->>WF: process end
  WF->>DB: status = PUBLISHED, version++
  WF->>Bus: CONFIG_PUBLISHED
```

***

## 4. Multi-Level Approval

Policies are **data**, not hardcoded BPMN per bank. One generic Camunda process (`WF_GENERIC_APPROVAL`) reads step definitions from the database at runtime.

### 4.1 Database model

**Table:** `approval_policy`

| Column        | Description                                                                  |
| ------------- | ---------------------------------------------------------------------------- |
| `policy_id`   | Unique ID                                                                    |
| `policy_code` | `CONFIG_PUBLISH_STANDARD`, `PARTNER_GO_LIVE`, `LIMIT_CHANGE_HIGH`            |
| `entity_type` | `integration_profile`, `customization_profile`, `job_def`, `payment_hold`, … |
| `scope_type`  | `GLOBAL`, `COUNTRY`, `PARTNER`                                               |
| `scope_id`    | e.g. `NG`, `ACME` (nullable for global)                                      |
| `condition`   | Optional SpEL/JSONata — e.g. `change.amount > 50000000`                      |
| `priority`    | First matching policy wins                                                   |
| `enabled`     | Boolean                                                                      |

**Table:** `approval_step_def`

| Column            | Description                           |
| ----------------- | ------------------------------------- |
| `step_id`         | Unique ID                             |
| `policy_id`       | FK                                    |
| `step_order`      | 1, 2, 3… (sequential levels)          |
| `step_name`       | Display label                         |
| `approver_type`   | `ROLE`, `GROUP`, `USER_LIST`          |
| `approver_ref`    | Keycloak role/group or JSON user list |
| `quorum_type`     | `ALL`, `ANY`, `N_OF_M`                |
| `quorum_count`    | For `N_OF_M` (e.g. 2 of 3)            |
| `sla_hours`       | Escalation timer                      |
| `escalation_role` | Role notified on SLA breach           |
| `allow_delegate`  | Boolean                               |

**Table:** `approval_request`

| Column                        | Description                                               |
| ----------------------------- | --------------------------------------------------------- |
| `request_id`                  | UUID                                                      |
| `policy_id`                   | FK                                                        |
| `entity_type`, `entity_id`    | What is being approved                                    |
| `submitted_by`                | Maker user ID                                             |
| `submitted_at`                | Timestamp                                                 |
| `status`                      | `PENDING`, `APPROVED`, `REJECTED`, `CANCELLED`, `EXPIRED` |
| `current_step_order`          | Active level                                              |
| `camunda_process_instance_id` | FK to engine                                              |
| `payload_snapshot`            | JSON diff / context for reviewers                         |

**Table:** `approval_action`

| Column         | Description                                |
| -------------- | ------------------------------------------ |
| `action_id`    | UUID                                       |
| `request_id`   | FK                                         |
| `step_order`   | Which level                                |
| `action`       | `APPROVE`, `REJECT`, `DELEGATE`, `COMMENT` |
| `performed_by` | Checker user ID                            |
| `performed_at` | Timestamp                                  |
| `comment`      | Optional                                   |

### 4.2 Example policies

| Policy code                      | Levels | Steps                                                                     |
| -------------------------------- | ------ | ------------------------------------------------------------------------- |
| `CONFIG_PUBLISH_STANDARD`        | 1      | Integration supervisor (`ANY`)                                            |
| `CONFIG_PUBLISH_PARTNER_GO_LIVE` | 2      | (1) Integration supervisor → (2) Country integration admin                |
| `LIMIT_CHANGE_HIGH`              | 3      | (1) Integration supervisor → (2) Country admin → (3) Platform super admin |
| `BULK_REPROCESS`                 | 2      | (1) Ops supervisor (`ANY`) → (2) Integration supervisor                   |
| `SQL_TASK_PROD`                  | 2      | (1) Integration supervisor → (2) DBA role (`ANY`)                         |
| `PAYMENT_HOLD_LARGE`             | 1      | Ops supervisor — triggered by Camel, not config                           |

### 4.3 Multi-level sequence

```mermaid
flowchart TD
  Submit[Maker submits] --> P1{Step 1 quorum met?}
  P1 -->|Reject| Rejected[REJECTED — entity stays DRAFT / held]
  P1 -->|Approve| P2{More steps?}
  P2 -->|No| Done[APPROVED — execute side-effect]
  P2 -->|Yes| P2wait[Step 2 task(s) created]
  P2wait --> P2check{Step 2 quorum met?}
  P2check -->|Reject| Rejected
  P2check -->|Approve| P3{More steps?}
  P3 -->|No| Done
  P3 -->|Yes| P3wait[Step 3…]
  Done --> Publish[Publish config / resume Camel / run job]
```

**Parallel quorum (N-of-M):** Step 2 with `quorum_type=N_OF_M`, `quorum_count=2`, three country admins assigned → Camunda multi-instance user tasks; process advances when 2 approve.

**Conditional policy:** Submitting a limit change evaluates `condition` on the policy; a ₦10M change uses 1-level policy, ₦100M uses 3-level policy — same `entity_type`, different `approval_policy` rows.

***

## 5. Workflow Catalog (Camunda Process IDs)

| Process ID                 | Trigger                        | Default policy                   | Side-effect on approve           |
| -------------------------- | ------------------------------ | -------------------------------- | -------------------------------- |
| `WF_GENERIC_APPROVAL`      | Any `ApprovalService.submit()` | From `approval_policy` resolver  | Callback to domain service       |
| `WF_CONFIG_PUBLISH`        | Config submit                  | `CONFIG_PUBLISH_*`               | `ConfigVersionService.publish()` |
| `WF_PARTNER_GO_LIVE`       | First prod publish             | `CONFIG_PUBLISH_PARTNER_GO_LIVE` | Publish + partner status ACTIVE  |
| `WF_MANUAL_PAYMENT_REVIEW` | Routing rule `MANUAL_REVIEW`   | `PAYMENT_HOLD_LARGE`             | Resume Camel route               |
| `WF_BULK_REPROCESS`        | Ops bulk retry API             | `BULK_REPROCESS`                 | Enqueue reprocess messages       |
| `WF_LIMIT_CHANGE`          | Limit policy update            | `LIMIT_CHANGE_HIGH`              | Publish limit config             |
| `WF_CERT_ROTATION`         | Cert expiry alert              | 1-level security officer         | Update `credential_ref`          |
| `WF_SQL_TASK`              | Prod SQL task publish          | `SQL_TASK_PROD`                  | Enable `sql_task_def`            |

`WF_GENERIC_APPROVAL` is the **template**; specific process IDs register typed completion listeners.

***

## 6. Camel Integration (Payment Hold)

```mermaid
sequenceDiagram
  participant Camel
  participant WF as h2h-workflow
  participant Camunda
  participant Ops as Ops Supervisor

  Camel->>Camel: routing_rule → MANUAL_REVIEW
  Camel->>WF: submit(PAYMENT_HOLD, correlationId, amount)
  WF->>Camunda: WF_MANUAL_PAYMENT_REVIEW
  Camunda->>Ops: Task in Operations Dashboard
  Ops->>WF: approve
  WF->>Camel: message correlationId to direct:processing
```

```java
// In payment route — amount threshold from DB routing_rule, not hardcoded
.choice()
    .when(simple("${exchangeProperty.h2h.manualReview} == true"))
        .to("camunda:submit?policyCode=PAYMENT_HOLD_LARGE")
        .to("camunda:await-approval")   // async wait — correlation on correlationId
    .otherwise()
        .to("finacle:postPayment")
.end()
```

***

## 7. Admin API

| Method | Path                       | Description                                       |
| ------ | -------------------------- | ------------------------------------------------- |
| POST   | `/publish/submit`          | Maker submits config → creates `approval_request` |
| GET    | `/approvals/pending`       | Checker inbox (filtered by role + scope)          |
| GET    | `/approvals/{id}`          | Request detail + diff + action history            |
| POST   | `/approvals/{id}/approve`  | Approve current step                              |
| POST   | `/approvals/{id}/reject`   | Reject — reverts entity to DRAFT                  |
| POST   | `/approvals/{id}/delegate` | Reassign if policy allows                         |
| GET    | `/approvals/policies`      | List `approval_policy` (platform admin)           |
| POST   | `/approvals/policies`      | Create/update policy + steps                      |

Publish Center ([05 Low-Code Admin Platform](/docs/05-low-code-admin-platform.md) §5.6) renders the checker inbox from `/approvals/pending`.

***

## 8. Events

| Event code                | When                                   |
| ------------------------- | -------------------------------------- |
| `APPROVAL_SUBMITTED`      | Maker submits                          |
| `APPROVAL_STEP_COMPLETED` | One level approved                     |
| `APPROVAL_COMPLETED`      | All levels approved — side-effect runs |
| `APPROVAL_REJECTED`       | Any checker rejects                    |
| `APPROVAL_SLA_BREACHED`   | Escalation timer fired                 |

Physical destinations from `event_channel_def` ([46 Database-Driven Events](/docs/46-database-driven-events.md)).

Checker assignment triggers `NotificationService.send()` with `APPROVAL_PENDING` template — see [48 Utility & Notification Integrations](/docs/48-utility-notification-integrations.md).

***

## 9. Creating a New Approval Flow (Checklist)

1. **Define entity** — what is being approved (`entity_type` in schema).
2. **Insert `approval_policy`** — scope, optional `condition`, link to `approval_step_def` rows (ordered levels + roles + quorum).
3. **Wire maker UI** — analyst screen calls domain `submit` API (config, job, script, etc.).
4. **Register completion handler** — `ApprovalCompletionHandler` bean for side-effect (publish, resume Camel, enable job).
5. **Add Camunda BPMN** (if not using `WF_GENERIC_APPROVAL`) — deploy via `ProcessDeployer` on startup.
6. **RBAC** — ensure Keycloak roles in `approver_ref` exist per [06 Personas](/docs/06-personas-and-rbac.md).
7. **Test** — maker submits → verify self-approval blocked → checker approves → side-effect + audit row.

**No JAR redeploy** for new policies or extra approval levels — only DB changes + optional BPMN deploy for novel side-effects.

***

## 10. Related Documents

* [04 Database-Driven Configuration](/docs/04-database-driven-configuration.md) — config lifecycle states
* [05 Low-Code Admin Platform](/docs/05-low-code-admin-platform.md) — Publish Center, approval UI
* [06 Personas and RBAC](/docs/06-personas-and-rbac.md) — maker vs checker roles
* [09 Security and Compliance](/docs/09-security-and-compliance.md) — segregation of duties
* [13 Design Patterns](/docs/13-design-patterns.md) — pattern 40 Segregation of Duties
* [22 Use Cases](/docs/22-use-cases-and-solutions.md) — UC-18 manual approval
* [30 Database Schema Reference](/docs/30-database-schema-reference.md) — `approval_*` tables
* [36 Admin API Reference](/docs/36-admin-api-reference.md) — `/approvals/*` endpoints


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://host2host.onibonje.com/docs/40-workflow-architecture.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
