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.

Approval rules
Shipped rules with threshold + concurrer + quorum.
At a glance — Default ships with 12 rules covering loans, savings, GL, members and infrastructure. You can tighten thresholds (lower KES amounts) freely; loosening them (raising thresholds above the regulatory minimum) is blocked at form-save.

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

Approval rule form
Rule form — domain, quorum, TTL, notification.

These determine when the rule fires.

FieldTypeExampleNotes
Rule codeChar (uppercase, unique)LOAN_DISBURSEUsed in code; cannot be edited after creation
ModelSelection of all mfi.dual.control.mixin modelsmfi.loanDriven by Python registry
Action methodCharaction_disburseMust be a method on the model
Amount fieldChar (technical name)amount_totalField on the model that holds the KES amount; blank = always gated
Threshold (KES)Float50,000.00Rule fires for amount > threshold; zero means always
Currency checkBooleanTrueIf True, only KES amounts are gated (multi-currency tenants)
Branch-awareBooleanTrueIf 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.

FieldTypeNotes
Checker groupMany2one res.groupsDefault mfi_group_checker; required
Authorised usersMany2many res.usersIf set, only these users (intersected with the group) may approve
Authorised branchesMany2many mfi.branchIf set, checker must be assigned to one of these branches
Min approversInteger1 by default; set to 2 for three-person rule (CEO + CRO + maker)
Maker excludedBoolean (always True)Hard-coded; not editable
Reporting-line excludedBooleanStrict 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.

FieldDefaultNotes
Escalation enabledFalseMaster switch
Escalation minutes60Wait period
Escalation actionnotifyOptions: notify, auto_reject, auto_approve (DANGEROUS — never use for money)
Escalation usersMany2many res.usersWhom to notify
Reminder cadence15 minHow 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:

  1. MFI > Two-person rule > Rules > New
  2. Pick the Model from the dropdown (registry lists only mixin-inheriting models)
  3. Type the exact Action method name (e.g. action_close)
  4. Set the Amount field if there is one; otherwise leave blank to gate always
  5. Set Threshold — start strict (e.g. 1,000) and relax as needed
  6. Choose Checker group on the Approvers tab
  7. Save — this itself triggers a RULE_CHANGE dual-control request for another admin to approve
  8. 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.

Warning — Regulatory minimums for Kenya: loan write-off threshold cannot exceed KES 0 (always gated); savings adjust cannot exceed KES 0; KYC override cannot be ungated. The form will refuse to save.

Worked scenarios

Scenario — Tightening the disbursement threshold after a near-miss

Setting: Tegemeo MFI, Eldoret head office, March 2026. A loan officer almost disbursed KES 80,000 to the wrong member because the names were similar (Wambui Kamau vs Wambui Kimani). Caught by the checker. CRO decides the threshold should drop from KES 50,000 to KES 25,000.

CharacterRole
Florence AchiengChief Risk Officer (maker)
Samuel KibetCEO (checker)
Jane WambuiLoan Officer (impact)
Peter OtienoInternal Auditor (observer)

Timeline

  1. Day 1, 09:15: Florence opens Rules > LOAN_DISBURSE (Form loads with threshold 50,000)
  2. Day 1, 09:17: She edits threshold to 25,000 and Saves (RULE_CHANGE request created, ID DCR-2026-00471)
  3. Day 1, 09:18: Samuel receives e-mail + SMS notification (Subject: "Approval required: LOAN_DISBURSE threshold change")
  4. Day 1, 09:24: Samuel opens Pending, reviews diff (50,000 → 25,000) (Diff view highlights changed field)
  5. Day 1, 09:25: Samuel clicks Approve (OTP 847291 SMS'd to +254722XXXXXX)
  6. Day 1, 09:26: Samuel enters OTP, confirms (Rule activates; Audit entry created with both signatures)
  7. Day 1, 10:00: Jane attempts to disburse KES 30,000 to Mary Mutua (Now caught by dual-control; previously would have auto-disbursed)
  8. 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.

CodeModel.actionThreshold KESChecker groupMin approvers
LOAN_DISBURSEmfi.loan.action_disburse50,000mfi_group_checker1
LOAN_WRITEOFFmfi.loan.action_write_off0mfi_group_cfo1
LOAN_WRITEOFF_BIGmfi.loan.action_write_off500,000mfi_group_cfo2
LOAN_RESCHEDULEmfi.loan.action_reschedule0mfi_group_credit_head1
SAV_ADJUSTmfi.savings.account.action_manual_adjust0mfi_group_finance1
GL_MANUALaccount.move.action_post_manual10,000mfi_group_finance1
MBR_BLACKLISTmfi.member.action_blacklist0mfi_group_compliance1
MBR_KYC_OVERRIDEmfi.member.action_force_kyc0mfi_group_compliance1
USR_PWDRESETres.users.action_reset_password0mfi_group_it_admin1
RULE_CHANGEmfi.two.person.rule.write0mfi_group_cro1
MPESA_ENVmfi.mpesa.provider.write0mfi_group_cto1
BANK_CLOSEaccount.journal.action_archive0mfi_group_cfo1
IFRS9_SIGNOFFmfi.ifrs9.run.action_sign_off0mfi_group_cfo1

Troubleshooting

SymptomLikely causeFix
Cannot find a model in the Model dropdown when creating a ruleThe target model does not inherit mfi.dual.control.mixin.Ask the developer to add `_inherit = ['mfi.dual.control.mixin', '']` and upgrade the module.
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 executeThe 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.

See also

Was this page helpful?