Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Identity and access

Explanation

Approvals and approval chains

How approval chains in modules/auth define ordered role steps, how other modules submit records for approval, and how approvers decide, defer, or cancel.

An approval chain (AuthApprovalChain) is an ordered list of steps for one resource and action, and each step names the role that must decide it. A business module that wants a record approved asks the auth module, over the EventBus, for the chain of its action and submits an approval request (AuthApprovalRequest). Approvers see their pending items in My Approvals (webapp/i18n/en.json key approvals.inbox_title) and approve, reject, or defer them. This page explains modules/auth/approval_chains/ and modules/auth/approvals/. Each business module's own approval states are documented on its module page.

In short

  • A chain is identified by resource and action; only an active, live chain is found (src/cluster_erp/modules/auth/approval_chains/models.py:13-61, src/cluster_erp/modules/auth/approval_chains/services.py:61-77).
  • A step has step_order, role_code, is_required, condition, and timeout_hours; step_order is unique per chain among live rows (src/cluster_erp/modules/auth/approval_chains/models.py:64-106).
  • A request has four statuses: pending, approved, rejected, and cancelled (src/cluster_erp/modules/auth/approvals/services.py:279, src/cluster_erp/modules/auth/approvals/services.py:379-400, src/cluster_erp/modules/auth/approvals/services.py:533).
  • The decider must hold the current step's role, must not be the requester, and must not have decided an earlier step of the same request; a system administrator role overrides the step role only (src/cluster_erp/modules/auth/approvals/services.py:311-368).
  • A final decision emits ApprovalCompleted on the EventBus (src/cluster_erp/modules/auth/approvals/routes.py:116-127).

How it works

Request states

The state diagram shows the life of one approval request.

stateDiagram-v2
 [*] --> pending: create_request
 pending --> pending: approved, next step exists
 pending --> approved: approved, no next step
 pending --> rejected: rejected
 pending --> cancelled: cancel by requester or administrator
 approved --> [*]
 rejected --> [*]
 cancelled --> [*]

In prose:

  1. create_request locks the chain row, requires the chain to be live, active, and to have at least one live step, and starts the request at the lowest live step_order with status pending (src/cluster_erp/modules/auth/approvals/services.py:229-285).
  2. decide locks the request row and refuses anything that is not pending (src/cluster_erp/modules/auth/approvals/services.py:287-309).
  3. It applies three segregation-of-duties checks: the requester cannot decide, a user who already decided a step of this request cannot decide again, and the decider must hold the current step's role_code unless the decider holds admin or system_admin (src/cluster_erp/modules/auth/approvals/services.py:311-368).
  4. It records an AuthApprovalDecision. A rejection ends the request. An approval moves current_step to the next live step, or ends the request as approved when no step follows (src/cluster_erp/modules/auth/approvals/services.py:370-405).
  5. cancel sets cancelled; only the requester or a system administrator may cancel (src/cluster_erp/modules/auth/approvals/services.py:514-537).

The decision body accepts only approved or rejected (src/cluster_erp/modules/auth/approvals/schemas.py:16).

Defer

defer records a personal snooze (AuthApprovalSnooze) until a future date. It does not change the request, does not advance a step, and does not hide the item from other approvers. The same eligibility as deciding applies, and a new snooze replaces the caller's previous one (src/cluster_erp/modules/auth/approvals/models.py:115-156, src/cluster_erp/modules/auth/approvals/services.py:407-489). lift_defer ends the caller's own snooze (src/cluster_erp/modules/auth/approvals/services.py:491-512). In the web app these actions are Defer and Bring back now (webapp/i18n/en.json keys approvals.defer, approvals.defer_lift).

The inbox

GET /api/v1/auth/approvals/inbox returns the caller's actionable pending requests. Eligibility (the step role held, or a system administrator) is filtered in SQL, and the list is capped at 200 items; the header X-Inbox-Truncated: true says that more exist (src/cluster_erp/modules/auth/approvals/routes.py:35-53, src/cluster_erp/modules/auth/approvals/services.py:48-215). Each item states whether the caller may decide (never on their own request), whether the caller holds the step role, and the caller's active snooze date (src/cluster_erp/modules/auth/approvals/services.py:184-213).

Editing a chain

Step changes are refused with HTTP 409 while the chain has a pending request. The guard takes the same chain row lock that create_request takes, so a submission and a step edit cannot interleave (src/cluster_erp/modules/auth/approval_chains/services.py:102-137, src/cluster_erp/modules/auth/approvals/services.py:236-251).

Routes

Route Permission Source
GET, POST /api/v1/auth/approval-chains/ approval_chain.read, approval_chain.create src/cluster_erp/modules/auth/approval_chains/routes.py:32-52
GET, PUT, DELETE /api/v1/auth/approval-chains/{chain_id} approval_chain.read, approval_chain.update, approval_chain.delete src/cluster_erp/modules/auth/approval_chains/routes.py:55-90
GET, POST /api/v1/auth/approval-chains/{chain_id}/steps approval_chain.read, approval_chain.update src/cluster_erp/modules/auth/approval_chains/routes.py:110-132
GET, PUT, DELETE /api/v1/auth/approval-steps/{step_id} approval_chain.read, approval_chain.update src/cluster_erp/modules/auth/approval_chains/routes.py:135-170
GET /api/v1/auth/approvals/pending permission.read src/cluster_erp/modules/auth/approvals/routes.py:25-32
GET /api/v1/auth/approvals/inbox authentication only src/cluster_erp/modules/auth/approvals/routes.py:35-53
POST /api/v1/auth/approvals/ permission.assign src/cluster_erp/modules/auth/approvals/routes.py:56-74
POST /api/v1/auth/approvals/{request_id}/decide authentication only; authority checked in the service src/cluster_erp/modules/auth/approvals/routes.py:90-131
POST, DELETE /api/v1/auth/approvals/{request_id}/defer authentication only; authority checked in the service src/cluster_erp/modules/auth/approvals/routes.py:134-168
POST /api/v1/auth/approvals/{request_id}/cancel authentication only; requester or administrator src/cluster_erp/modules/auth/approvals/routes.py:171-186
GET /api/v1/auth/approvals/entity/{entity_type}/{entity_id} permission.read src/cluster_erp/modules/auth/approvals/routes.py:189-198

Why it is built this way

  • The project's written engineering rules require inter-module communication through the EventBus; the auth module enforces this with its single public door modules/auth/guards.py (src/cluster_erp/modules/auth/guards.py:22-27), and other modules reach approvals only through the contracts below.
  • The decide route carries no administrative permission because the authority to decide is the step role, checked in the service (src/cluster_erp/modules/auth/approvals/routes.py:98-101).
  • The request row lock exists because two approvers, or a double click, must not both advance the same step (src/cluster_erp/modules/auth/approvals/services.py:294-297).
  • A step's is_required and condition are not evaluated in decide; the comment states that skipping them only ever requires more approvals (src/cluster_erp/modules/auth/approvals/services.py:355-356).
  • The snooze is inbox state, not a decision, so the decision audit trail records only real outcomes (src/cluster_erp/modules/auth/approvals/models.py:115-122).

Rules and boundaries

  • A requester never decides or defers their own request (src/cluster_erp/modules/auth/approvals/services.py:313-318, src/cluster_erp/modules/auth/approvals/services.py:418-423).
  • A chain must have at least one live step before a request can be created (src/cluster_erp/modules/auth/approvals/services.py:258-273).
  • Requests, decisions, and snoozes are soft-deleted rows like every other record (src/cluster_erp/modules/auth/approvals/services.py:479-482).

Interaction with other parts

Direction Contract Handler or caller Source
auth answers query GetApprovalChainForAction Module._handle_get_approval_chain src/cluster_erp/modules/auth/module.py:83-86, src/cluster_erp/modules/auth/module.py:146-164
auth answers query SubmitForApproval Module._handle_submit_for_approval src/cluster_erp/modules/auth/module.py:87, src/cluster_erp/modules/auth/module.py:166-188
auth answers query GetApprovalRequestById Module._handle_get_approval_request src/cluster_erp/modules/auth/module.py:88-91, src/cluster_erp/modules/auth/module.py:190-211
auth emits ApprovalCompleted decide_approval src/cluster_erp/modules/auth/approvals/routes.py:116-127
documents asks and submits GetApprovalChainForAction, SubmitForApproval document version submit route src/cluster_erp/modules/documents/documents/routes.py:326-350
documents subscribes ApprovalCompleted handle_approval_completed src/cluster_erp/modules/documents/module.py:74-77
intranet leave asks and submits GetApprovalChainForAction, SubmitForApproval _route_approval src/cluster_erp/modules/intranet/leave/services.py:740-770
intranet requests asks and submits GetApprovalChainForAction, SubmitForApproval requests service src/cluster_erp/modules/intranet/requests/services.py:292-305

The contract classes are in src/cluster_erp/core/contracts.py:275-309. See contracts reference.

Limits and open points

  • timeout_hours is stored on a step but no code reads it; a grep for timeout_hours outside the approval chain model, schema, and metadata files finds nothing.
  • is_required and condition are stored but not evaluated (src/cluster_erp/modules/auth/approvals/services.py:355-356).
  • AuthApprovalSnooze is not exported by the aggregator modules/auth/models.py; it is registered when the approvals service module is imported (src/cluster_erp/modules/auth/models.py:1-42, src/cluster_erp/modules/auth/approvals/services.py:22-26).

Terms introduced

Term Code identifier Definition Turkish term Source
approval chain AuthApprovalChain An ordered list of role steps for one resource and action. Onay Zinciri src/cluster_erp/modules/auth/approval_chains/models.py:13
approval step AuthApprovalStep One position in a chain, decided by holders of its role. onay adımı src/cluster_erp/modules/auth/approval_chains/models.py:64
approval request AuthApprovalRequest One record submitted to a chain, with its current step and status. onay talebi src/cluster_erp/modules/auth/approvals/models.py:26
approval snooze AuthApprovalSnooze A personal defer mark that hides a pending item from one approver until a date. erteleme src/cluster_erp/modules/auth/approvals/models.py:115
segregation of duties AUTH_SELF_APPROVAL_FORBIDDEN The rule that a requester or an earlier decider may not decide a step. görevler ayrılığı src/cluster_erp/modules/auth/approvals/services.py:311

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13