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
resourceandaction; 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, andtimeout_hours;step_orderis 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, andcancelled(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
ApprovalCompletedon 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:
create_requestlocks 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 livestep_orderwith statuspending(src/cluster_erp/modules/auth/approvals/services.py:229-285).decidelocks the request row and refuses anything that is notpending(src/cluster_erp/modules/auth/approvals/services.py:287-309).- 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_codeunless the decider holdsadminorsystem_admin(src/cluster_erp/modules/auth/approvals/services.py:311-368). - It records an
AuthApprovalDecision. A rejection ends the request. An approval movescurrent_stepto the next live step, or ends the request asapprovedwhen no step follows (src/cluster_erp/modules/auth/approvals/services.py:370-405). cancelsetscancelled; 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_requiredandconditionare not evaluated indecide; 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_hoursis stored on a step but no code reads it; agrepfortimeout_hoursoutside the approval chain model, schema, and metadata files finds nothing.is_requiredandconditionare stored but not evaluated (src/cluster_erp/modules/auth/approvals/services.py:355-356).AuthApprovalSnoozeis not exported by the aggregatormodules/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 |
Related pages¶
- Identity, access, tenancy, privacy, and licensing
- Roles and permissions
- Auth module
- Documents module
- Home widgets
Verified against commit 92f8051 on 2026-11-13.