Segregation of Duties¶
Applies to: All subscriptions
Purpose¶
Explain the maker-checker model, state precisely what changes when it is relaxed, and make clear whose decision that is. This setting is one of the first things an auditor asks about, and "it was easier during testing" is not an answer anyone wants to give.
Audience¶
The customer's finance control owner and internal audit, primarily. System administrators, who make the change. Implementation consultants, who must not make it unilaterally.
Prerequisites¶
- Roles and Permissions
- The customer's own internal control policy
Reference¶
The model¶
Segregation of duties separates preparation from commitment. The person who prepares a document is not the person who commits it.
| Function | Role | Can |
|---|---|---|
| Maker | Tax Executive | Create, edit, correct, print, view |
| Checker | Tax Controller | Everything the maker can, plus generate, cancel, approve, issue, promote |
The control exists because the acts on the checker's side are irreversible or externally visible: registering an invoice with the government, cancelling a registration, issuing a document, promoting a template that changes how every future document is interpreted.
Four further separations are built into the role set:
| Separation | Effect |
|---|---|
| Administration from transactions | An org admin manages users and configuration but cannot commit a transaction |
| Logistics from tax | Logistics roles hold the e-Way Bill family but cannot register invoices |
| Assurance from operations | The auditor role can view and print, and nothing else |
| Sign-off from processing | The Approver role holds invoice.approve and no transactional permission, so approval authority can be granted without processing authority |
The two role axes¶
Approval in Complifly is governed by two independent fields on the user record, and both are required. Setting one without the other is the most common approval misconfiguration, and it is silent — no error, no log entry, no refused request.
| System role | Approval role | |
|---|---|---|
| Stored in | sys_user.role |
sys_user.approval_role_key |
| Governed by | config/policies.js — the permission matrix |
dbo.mst_approval_role — the seniority ladder |
| Answers | May this person call the approve endpoint? | Which level of a multi-level approval are they? |
| Values that can approve | Approver, Tax Controller (not Super Admin, which does not approve customer documents) | Finance Executive, Finance Manager, Finance Controller, Business Head, Plant Head, Director, CFO |
| Missing → | 403 on the approve call; no button in the UI |
The user is never named by any level, so nothing reaches them |
The approval ladder¶
Seven levels, junior to senior. The rank matters: when several rules fire on one document, the levels they require are merged and ordered by rank.
| Rank | Approval role | Typically |
|---|---|---|
| 10 | finance_executive |
AP/AR staff |
| 20 | finance_manager |
Team lead, routine value |
| 30 | finance_controller |
Owns the ledger; the usual second signature |
| 40 | business_head |
Commercial owner of the relationship |
| 40 | plant_head |
Same rank — a peer, not a further step |
| 50 | director |
Board-level sign-off |
| 60 | cfo |
Highest step |
Assign both fields together, on Administration > Admin & Logs > Users & Roles. The ladder is also served by GET /api/admin/approval-roles for anyone provisioning users programmatically.
A level with no holder stalls silently
Nothing checks that somebody holds each approval role the matrix uses. If exactly one person holds finance_controller and they leave, every document routed through that level stops, with no alert. Assign each level used by your matrix to at least two people — any holder can clear it, and the history records which one did.
Controls the approval workflow enforces regardless of role¶
| Control | Behaviour |
|---|---|
| Self-approval | Whoever submitted a document cannot decide it, whatever role they hold. Checked on identity, not role, and not waivable — not by an org admin, and a Super Admin does not hold the approve permission at all |
| Sequence | Levels are decided in order. Level 2 cannot act before level 1 has |
| Administrative override | An Org Administrator, who alone holds approval.override, may force-approve or force-reject, but must give a written reason, and every remaining level is stamped OVERRIDDEN rather than APPROVED — a bypass is legible as a bypass. It is refused on a document that administrator submitted (SELF_OVERRIDE), so a second person is always involved. Super Admin does not hold it |
One decision for the company, locked when live work starts¶
Segregation of duties is decided once, for the whole company, at onboarding. The customer and Complifly agree the setting; onboarding records it against the company (with the reason, if it is relaxed) and the onboarding wizard never writes it per registration. There is no per-GSTIN setting: FEATURE_SCOPE_NOT_ALLOWED refuses one. Overrides that already existed when this rule shipped are kept and still apply, so nothing changed for anyone on the day; they can be removed, and no new one can be added.
The setting locks against relaxing when the company starts live work. The lock is read from the company's activity when someone tries to relax it, not stored, so no path can forget to engage it:
| Counts as live work | Does not count |
|---|---|
| An IRN issued | Work on a registration whose GSP configuration is explicitly SANDBOX (the registration's own row first, else the company's) |
| An e-way bill generated | A cancelled approval (a system act, not a decision) |
| An approval decided (approved or rejected) | A pending approval |
A company with no GSP configuration row at all has no recorded environment, so its work does count. The lock errs towards engaging: a wrongly locked company costs a Super Admin request; a wrongly open one is the drift the lock exists to stop.
| Who and what | Before live work | After live work |
|---|---|---|
| Org Admin turns it on | Allowed | Allowed, no reason |
| Org Admin turns it off | Allowed, with a reason; other Org Admins are told | 403 SOD_LOCKED, nothing changed, the attempt is audited |
| Super Admin turns it off | Allowed, with a reason | Allowed with a reason and the customer's approval reference (approvalRef); 400 SOD_APPROVAL_REF_REQUIRED without it |
Turning it on is never refused, because it takes rights away from makers and gives nobody any. The Operational Controls screen asks for confirmation first and says how many active Tax Controllers the company has: if there are none, nobody could approve, issue or cancel until one is added, and after the lock turning it off again needs Complifly.
Which direction a change goes is decided by each registration's effective value, not by which call was made. Removing an old GSTIN override that enforced, under a company default of off, relaxes that registration and is locked like any other relaxation. Resetting a relaxed company default to the platform default (on) tightens and is open.
The customer's approval reference is a record, not a check. It is a ticket number, an e-mail subject or a signed form number, 3 to 120 characters. Complifly stores it at the front of the reason in Audit History, writes it to the ACCESS_WIDENED entry as customerApprovalRef, and sends it to the customer's other Org Admins, with the statement that Complifly, not a company administrator, made the change. Keep the evidence it points to.
Relaxing it needs a reason, and your other administrators are told¶
You cannot relax segregation of duties without a written reason, and your company's other Org Admins are emailed when you do. This applies to every caller, a Complifly Super Admin included, and to every route that can relax it: setting the company default to off and removing an older override that was keeping a registration enforced while the company default is off.
| What happens | Detail |
|---|---|
| The request carries no reason | 400 ACCESS_WIDENING_REASON_REQUIRED. Nothing is changed, no approval is cancelled, nobody is emailed |
| The request carries a reason | The change is saved, the reason is stored in the control's Audit History, and an ACCESS_WIDENED entry is written to the customer's audit log |
| The notice | One email to the company's other active Org Admins, naming who, what, which registrations, the reason, and what relaxing means. The person who made the change is not emailed. A bulk change sends one email per company, not one per registration |
| Nobody else to tell | A company with a single Org Admin: the change still goes through and the entry records NO_OTHER_ADMIN |
| The email cannot be sent | The change still stands and the entry records EMAIL_NOT_CONFIGURED or SEND_FAILED. The notice is best-effort so an email outage cannot block a reasoned change; the record never is |
| Tightening, or no effective change | Switching it on, or writing "off" on a registration that is already off, needs no reason and sends nothing |
This email is not the Attention Center's notification switch. It does not depend on NOTIFICATION_DISPATCH_ENABLED (which defaults off): a company must not be able to mute a notice that its own approval controls were relaxed. It only ever goes to that company's own Org Admins.
Everything done under the relaxation is marked. An audit entry written during a request that was permitted only because segregation of duties was relaxed for that registration carries accessBasis: SOD_RELAXED and the permissions that needed it (accessBasisPermissions). A Tax Controller, who holds the right either way, is not marked. That is how an auditor separates actions taken under the designed model from actions taken under the relaxation, without reconstructing what the setting was on that date. Entries from before 15 October 2026 carry no mark.
What relaxing it does¶
Setting SOD_ENFORCED to off collapses maker permissions into checker permissions. Specifically:
| Role | Gains, when relaxed |
|---|---|
| Tax Executive | The full checker set — generate, cancel, approve, issue, promote |
| Org Admin | The full checker set, adding transactional authority to configuration authority |
| Transport Manager, Logistics Operator | The full checker set, including IRN generation |
| Approver | Nothing. Deliberately excluded from the collapse |
Note the breadth. Relaxation is not a narrow adjustment to one workflow: it grants transactional authority to roles that were deliberately built without it, including logistics and administration.
Approver is excluded on purpose. An approver who inherited the checker set would gain invoice.create and invoice.issue — the ability to raise a document and then sign it off, which is the single outcome the role was added to prevent. A tenant running in SME mode therefore keeps this one separation even though it has relaxed the others.
Two approval controls also survive relaxation entirely: the approval level must still match, and nobody can approve a document they submitted. Relaxation widens who can be asked to approve; it never lets a level be skipped.
When relaxation is legitimate¶
There is a real case for it. A small business where one person genuinely performs preparation and commitment — because there is no second person — gains nothing from a control that only produces a blocked screen. The setting exists for that situation.
It is not legitimate as:
- A way past an approval step that is slowing down user acceptance testing
- A default applied because nobody decided
- A temporary measure at go-live that is never reviewed
The distinction is whether a control owner decided, with the consequence understood.
Whose decision it is¶
The customer's finance control owner, not the implementation team and not the system administrator who makes the change.
Record:
| Record | Why |
|---|---|
| Who decided | An auditor will ask |
| When | To establish the period affected |
| The business reason | "No second person available" is a reason; "faster" is not |
| Compensating controls, if any | Review of issued documents, for example |
| Review date | Circumstances change as an organisation grows |
If relaxation is not acceptable but staffing is thin¶
Options short of relaxing the setting:
| Option | Trade-off |
|---|---|
| One checker covering several areas | Concentrates authority in one person, but keeps preparation separate |
| A checker outside the immediate team | Slower, but preserves the control |
| Keep it enforced and name a second person | A second user in the Tax Controller or Tax Executive role keeps the separation, at the cost of one more account |
| A separate company for the small entity | The setting is decided per company, so an entity that genuinely cannot separate duties can sit in its own company with its own decision. This is an onboarding decision, not a switch |
| Relax with a compensating review | Documented review of committed documents |
The setting is no longer per registration: a group cannot keep one registration enforced and relax a sibling inside the same company. If that was how a group was set up before, the older override still applies and can be removed, but not replaced.
Validation¶
| Check | Method | Pass condition |
|---|---|---|
| Current state known | Check the setting per GSTIN | Known for every registration, not assumed |
| Enforcement works | With it on, sign in as a tax executive and attempt to generate an IRN | Refused |
| Relaxation works as documented | With it off in a test environment, repeat | Permitted |
| Breadth understood | With it off, check what a logistics operator can now do | Confirm the customer intended to grant transactional authority to logistics |
| Decision recorded | Change record | Names the decision-maker, date and reason |
| Decided once for the company | Operational Controls > Company Defaults, and the row's lock summary | Known and recorded; the row says whether the company is locked, and since when |
| No older per-GSTIN override remains | Operational Controls > GSTIN Overrides, or node scripts/sod_inventory.js |
Any left is deliberate and understood; none can be added |
| Compensating controls in place | Where relaxed, confirm the agreed review is happening | Evidenced, not merely planned |
| Review scheduled | Operations calendar | A date to revisit the decision |
Troubleshooting¶
| Symptom | Cause | Action |
|---|---|---|
| A maker cannot generate an IRN | Segregation enforced. Working as designed | Explain the model. Do not relax the setting to resolve a single ticket |
| A maker can suddenly generate an IRN | Segregation relaxed | Confirm this was a recorded decision. If not, re-enable and investigate who changed it |
| A logistics user can register invoices | Relaxation collapsed their permissions into the checker set | Expected effect. If unintended, this is the strongest argument for re-enabling |
| Auditor asks who authorised relaxation | Look in the Audit Logs for ACCESS_WIDENED (who, when, which registrations, the reason, who was emailed) and in the control's Audit History |
Changes made before 15 October 2026 carry no reason; for those, the control's history shows who and when only |
| Approvals block a go-live | Approval thresholds, not segregation | Check the Invoice Generation thresholds first; they are a separate control |
| One registration behaves differently | An older GSTIN override from before the setting became company-wide | Confirm it is deliberate. Operational Controls > GSTIN Overrides shows it as an older override; it can be reset, not replaced |
An Org Admin gets SOD_LOCKED when switching it off |
The company has started live work (a live IRN, a generated e-way bill or a decided approval) | Expected. Turning it on is still open. To relax it, a Complifly Super Admin needs a reason and the customer's approval reference |
| A company still in UAT is already locked | Its test work is not on a SANDBOX GSP configuration, or it has no GSP configuration row, so its work counts |
Record the test period's GSP configuration as SANDBOX before the first test document. The lock reads the current GSP configuration and each server remembers a "locked" answer until it restarts, so relabelling afterwards is not a reliable way to unlock; use the Super Admin exception |
A Super Admin gets SOD_APPROVAL_REF_REQUIRED |
The company is locked and the request carried no usable approvalRef |
Get the customer's approval in writing and send its reference with the reason |
| Turning it on left nobody able to issue | The company had no active Tax Controller | Add one under Users & Roles. Turning it off again needs Complifly once the company is locked |
| Relaxed during testing and never reviewed | The classic failure | Put a review date in the reason you give when relaxing; it is recorded and emailed to the other administrators |
| "ACCESS_WIDENING_REASON_REQUIRED" when switching it off | No reason was given | Give one. See above |
| Customer wants partial relaxation for one action | Not supported — relaxation is all or nothing for the company | Consider a role change with the consequence accepted, or a second user in the checker role |
| An approver sees the workflow panel but no Approve button | Their system role does not hold invoice.approve |
Assign the Approver system role (or Tax Controller). The approval role alone is not enough |
| An approver can approve, but no document ever reaches them | No approval_role_key on their user record, or a different one |
Check both fields on the user. The panel names the role the document is waiting on |
| A document sits PENDING with nobody able to act | The level names an approval role no active user holds | Assign that approval role to someone. This is why levels want two holders |
A senior approval appears in the history as OVERRIDDEN |
It was taken through Force Approve rather than by the named approver | Expected if an administrator bypassed. If it is happening routinely, the approvers are not set up — fix the roles rather than normalising the override |
| A user cannot approve their own document | Self-approval is blocked on identity and is not waivable | Correct behaviour. Have another holder of the level decide, or have a different Org Administrator use Force Approve with a reason |
Force Approve answers SELF_OVERRIDE |
You submitted this document, and Override refuses the person who submitted it | Ask another Org Administrator. If you are the only one, a second administrator is needed |
| A Super Admin cannot approve or override anything | Super Admin does not hold invoice.approve, tds.checker or approval.override: approving a customer's document is the customer's decision |
Correct behaviour. Use a customer role that holds the permission |
Related Articles¶
- Roles and Permissions — the permission sets that collapse
- Feature Flags — where the setting lives
- Audit Log Review — evidencing control operation
- Invoice Generation Module — approval thresholds, a separate control