Configuring two-person rules
Rules are the configuration heart of maker-checker. Each rule maps a (model, action) pair to a threshold, a checker group, and optional escalation. Edit them carefully — every change is itself dual-controlled.

Anatomy of a rule
Open MFI > Two-person rule > Rules and select any row. The form has five tabs: Trigger, Approvers, Escalation, OTP, Audit.
- Trigger — which (model, action) fires the rule; the KES field to read; the threshold above which dual-control is required
- Approvers — which security groups or named users may approve; optionally an Authorised branches list
- Escalation — what happens after N minutes of no action (notify CRO, auto-reject, etc.)
- OTP — OTP delivery channel (SMS, e-mail, both) and lifetime
- Audit — read-only counters, last 30 approvals, rejection rate
Trigger tab fields

These determine when the rule fires.
| Field | Type | Example | Notes |
|---|---|---|---|
| Rule code | Char (uppercase, unique) | LOAN_DISBURSE | Used in code; cannot be edited after creation |
| Model | Selection of all mfi.dual.control.mixin models | mfi.loan | Driven by Python registry |
| Action method | Char | action_disburse | Must be a method on the model |
| Amount field | Char (technical name) | amount_total | Field on the model that holds the KES amount; blank = always gated |
| Threshold (KES) | Float | 50,000.00 | Rule fires for amount > threshold; zero means always |
| Currency check | Boolean | True | If True, only KES amounts are gated (multi-currency tenants) |
| Branch-aware | Boolean | True | If True, threshold applies per-branch; else global |
Approver tab fields
These constrain who can hit Approve. By default it's the security group; you can pin specific users for sensitive rules.
| Field | Type | Notes |
|---|---|---|
| Checker group | Many2one res.groups | Default mfi_group_checker; required |
| Authorised users | Many2many res.users | If set, only these users (intersected with the group) may approve |
| Authorised branches | Many2many mfi.branch | If set, checker must be assigned to one of these branches |
| Min approvers | Integer | 1 by default; set to 2 for three-person rule (CEO + CRO + maker) |
| Maker excluded | Boolean (always True) | Hard-coded; not editable |
| Reporting-line excluded | Boolean | Strict segregation — checker cannot be in maker's manager chain |
Escalation tab
If no checker has acted within N minutes, escalation kicks in. Common for after-hours disbursements.
| Field | Default | Notes |
|---|---|---|
| Escalation enabled | False | Master switch |
| Escalation minutes | 60 | Wait period |
| Escalation action | notify | Options: notify, auto_reject, auto_approve (DANGEROUS — never use for money) |
| Escalation users | Many2many res.users | Whom to notify |
| Reminder cadence | 15 min | How often to nudge until acted on |
Creating a new rule
To gate a new action, the model must inherit mfi.dual.control.mixin. With that in place:
- MFI > Two-person rule > Rules > New
- Pick the Model from the dropdown (registry lists only mixin-inheriting models)
- Type the exact Action method name (e.g. action_close)
- Set the Amount field if there is one; otherwise leave blank to gate always
- Set Threshold — start strict (e.g. 1,000) and relax as needed
- Choose Checker group on the Approvers tab
- Save — this itself triggers a RULE_CHANGE dual-control request for another admin to approve
- Test in the staging tenant before approving in production
Editing thresholds
Editing threshold downward (e.g. KES 50,000 → KES 25,000) is fast — save fires a RULE_CHANGE request, a peer approves with OTP, and it goes live. Editing upward is blocked above the regulatory minimums.
Worked scenarios
Scenario — Tightening the disbursement threshold after a near-miss
| Character | Role |
|---|---|
| Florence Achieng | Chief Risk Officer (maker) |
| Samuel Kibet | CEO (checker) |
| Jane Wambui | Loan Officer (impact) |
| Peter Otieno | Internal Auditor (observer) |
Timeline
- Day 1, 09:15: Florence opens Rules > LOAN_DISBURSE (Form loads with threshold 50,000)
- Day 1, 09:17: She edits threshold to 25,000 and Saves (RULE_CHANGE request created, ID DCR-2026-00471)
- Day 1, 09:18: Samuel receives e-mail + SMS notification (Subject: "Approval required: LOAN_DISBURSE threshold change")
- Day 1, 09:24: Samuel opens Pending, reviews diff (50,000 → 25,000) (Diff view highlights changed field)
- Day 1, 09:25: Samuel clicks Approve (OTP 847291 SMS'd to +254722XXXXXX)
- Day 1, 09:26: Samuel enters OTP, confirms (Rule activates; Audit entry created with both signatures)
- Day 1, 10:00: Jane attempts to disburse KES 30,000 to Mary Mutua (Now caught by dual-control; previously would have auto-disbursed)
- Day 1, 10:05: Branch manager Aisha Hassan approves (Loan disburses normally)
Outcome — Threshold tightened in 11 minutes with full audit trail. Internal audit logs the change as evidence for the quarterly SASRA inspection.
Reference
Default rule catalogue
All defaults shipped with mfi_base 18.0.5.0.0.
| Code | Model.action | Threshold KES | Checker group | Min approvers |
|---|---|---|---|---|
| LOAN_DISBURSE | mfi.loan.action_disburse | 50,000 | mfi_group_checker | 1 |
| LOAN_WRITEOFF | mfi.loan.action_write_off | 0 | mfi_group_cfo | 1 |
| LOAN_WRITEOFF_BIG | mfi.loan.action_write_off | 500,000 | mfi_group_cfo | 2 |
| LOAN_RESCHEDULE | mfi.loan.action_reschedule | 0 | mfi_group_credit_head | 1 |
| SAV_ADJUST | mfi.savings.account.action_manual_adjust | 0 | mfi_group_finance | 1 |
| GL_MANUAL | account.move.action_post_manual | 10,000 | mfi_group_finance | 1 |
| MBR_BLACKLIST | mfi.member.action_blacklist | 0 | mfi_group_compliance | 1 |
| MBR_KYC_OVERRIDE | mfi.member.action_force_kyc | 0 | mfi_group_compliance | 1 |
| USR_PWDRESET | res.users.action_reset_password | 0 | mfi_group_it_admin | 1 |
| RULE_CHANGE | mfi.two.person.rule.write | 0 | mfi_group_cro | 1 |
| MPESA_ENV | mfi.mpesa.provider.write | 0 | mfi_group_cto | 1 |
| BANK_CLOSE | account.journal.action_archive | 0 | mfi_group_cfo | 1 |
| IFRS9_SIGNOFF | mfi.ifrs9.run.action_sign_off | 0 | mfi_group_cfo | 1 |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Cannot find a model in the Model dropdown when creating a rule | The target model does not inherit mfi.dual.control.mixin. | Ask the developer to add `_inherit = ['mfi.dual.control.mixin', ' |
| Save fails with "Threshold above regulatory minimum" | Kenyan regulation requires this action to be always-gated. | Leave threshold at 0. If you need fewer gates, document the business case and escalate to CRO; you cannot override at the form layer. |
| Checker approves but action does not execute | The original action method has been renamed or removed by a customisation. | Check the server log — you will see AttributeError. Update the rule's Action method to match the new code name. |

