Mapping Audit
The Mapping Audit page is what makes the Migration Importer auditable. Every committed record carries its lineage; this page surfaces that lineage as a searchable, exportable, internal-auditor-friendly view that keeps working long after the migration team has moved on.

What is audited
Mapping Audit captures three flavours of records: (1) one row per production record created by the Migration Importer, (2) one row per warning acknowledgement, and (3) one row per bulk-edit on staging or post-commit override. Every row references the Source, Run, staging row hash, and the production record id where applicable.
| Record type | Captures | Typical volume for a Kenyan SACCO migration |
|---|---|---|
| Production lineage | Source -> Run -> Staging row -> Production record | Sum of committed records across all Runs (~200,000 for a 25k-member SACCO) |
| Warning acknowledgement | Rule code, severity, reason, signer, timestamp | 300-2,000 per migration |
| Manual override | Staging or production field edited by hand, before/after value, signer | 20-200 per migration |
| Reverse commit event | Run id, reason, signer, downstream-conflict report | 0-2 per migration |
The lineage view on production records
Every production record migrated by the importer exposes a Migration tab on its form. The tab shows lineage details and a link to the Mapping Audit row.
- Source: 'Tujikomboe — Members — v2'
- Run: 'TJK-MEM-003' (link)
- Source key: 'M-00421' (original Loan Performer MemberNo)
- Staging hash: SHA-256 of the original CSV row content, displayed as the first 12 characters
- Committed by: Jane Wambui at 2026-01-13 22:04:11
- Acknowledged warnings on this record: list of (rule_code, reason, signer)
- Manual overrides on this record: list of (field, before, after, signer, timestamp)
Searching the audit
The Mapping Audit list view supports the filters an auditor will most commonly use.
| Question the auditor is asking | Filter to use |
|---|---|
| Where did this member's opening balance come from? | Production record id = res.partner X — opens lineage card |
| Show every member migrated from Loan Performer with KYC_MISSING_LARGE_BALANCE acknowledged. | Source.legacy_system = Loan Performer AND Acknowledgement.rule_code = KYC_MISSING_LARGE_BALANCE |
| List every manual override done by Peter Otieno during migration. | Manual Override AND Signer = Peter Otieno |
| Show the full reverse-commit trail for tenant X. | Type = Reverse Commit AND Tenant = X |
| Show every commit signed off by Florence Achieng. | Type = Production Lineage AND Counter-signed by = Florence Achieng |
Export formats for auditors and regulators
The audit can be exported for external auditors (KPMG, PwC, Mazars) and for SASRA inspection visits. Three formats are supported.
- CSV — flat dump of the selected filter, suitable for spreadsheet analysis.
- PDF — formatted report with header, signatures, source list and exception summary. Used as an exhibit in audit binders.
- JSON — full structured export including staging hashes. Used by internal data science to reconstruct migration deltas.
Retention and immutability
Mapping Audit records cannot be edited or deleted by any user, including system administrators, through the normal BridgeERP UI. They can only be modified by a database administrator with shell access — and any such modification triggers a SOC 2 alert and a Slack notification to the audit committee.
- Retention: 10 years from the date of the Migration Run, or 7 years post account closure for member-specific records, whichever is later. Matches CBK Prudential Guideline section on record retention.
- Backup: nightly database backup includes mapping_audit; weekly offsite snapshot is taken to a separate S3 bucket retained for the full 10 years.
- Hash chain: every audit row stores a hash of itself plus the previous row's hash, forming a tamper-evident chain. Auditor can request a chain-verification report at any time.
Worked scenarios
Scenario — Tujikomboe responds to a SASRA inspection in November 2027
| Character | Role |
|---|---|
| Peter Otieno | Tujikomboe Operations Manager |
| Florence Achieng | Tujikomboe Accountant |
| SASRA Inspector | External regulator |
Timeline
- Nov 2027, Day 1, 10:00: Inspector arrives, requests the dataset. (Peter opens Mapping Audit, filters: Source.legacy_system = Loan Performer AND Acknowledgement.rule_code = KYC_MISSING_LARGE_BALANCE.)
- Nov 2027, Day 1, 10:05: Filter returns 87 members. (Each row links to res.partner with current KYC state.)
- Nov 2027, Day 1, 10:30: Peter exports the 87 to PDF. Each row shows: opening balance, acknowledgement reason ('Bria-led KYC capture plan'), date KYC was completed post go-live, current balance. (PDF stamped with retention chain hash.)
- Nov 2027, Day 1, 14:00: Inspector asks: 'Have any of these 87 transacted before their KYC was completed?' (Peter runs a cross-query: filter savings.transaction by member in the 87 and date < member.kyc_completed_at.)
- Nov 2027, Day 1, 14:15: Cross-query returns 4 members who deposited (no withdrawals). 0 withdrew before KYC. (Peter prints the cross-query results.)
- Nov 2027, Day 1, 15:00: Inspector accepts: deposits without KYC are within risk appetite; the absence of withdrawals before KYC satisfies the inspection. (Inspection closes for this control.)
Outcome — SASRA inspection cleared on this control in under a day, thanks to lineage that survived 22 months. The PDF export becomes an exhibit in the inspection report. Mapping Audit pays for itself in one query.
Reference
Mapping Audit row fields
Every row has these fields. None are editable through the UI.
| Field | Type | Description |
|---|---|---|
| id | Integer | Sequential audit id. |
| audit_type | Selection | production_lineage | warning_ack | manual_override | reverse_commit | export |
| tenant_id | Many2one company | Tenant under audit. |
| source_id | Many2one mfi.migration.source | Source the row originated from. |
| source_version | Char | v1, v2, ... — frozen at time of run. |
| run_id | Many2one mfi.migration.run | Run that processed the row. |
| staging_table | Char | e.g. mfi.migration.staging.member |
| staging_hash | Char (64) | SHA-256 of the original row content. |
| production_model | Char | e.g. res.partner |
| production_id | Integer | Id of the production record. |
| source_key | Char | Natural key from legacy system. |
| actor_id | Many2one res.users | Who triggered the event. |
| counter_signer_id | Many2one res.users | Who counter-signed (if applicable). |
| actor_at | Datetime | Timestamp. |
| details_json | Text (JSON) | Type-specific payload (e.g. for warning_ack: rule_code, reason, severity). |
| prev_hash | Char (64) | Hash of the previous audit row. |
| this_hash | Char (64) | SHA-256 of this row's content + prev_hash. |
details_json shapes by audit_type
The details_json is type-specific.
| audit_type | Shape |
|---|---|
| production_lineage | {record_count: 1, two_person: bool, ks_total: float} |
| warning_ack | {rule_code: str, severity: str, reason: str, exception_id: int} |
| manual_override | {field: str, before: any, after: any, scope: 'staging'|'production'} |
| reverse_commit | {reason: str, downstream_conflicts: [...], original_run_id: int} |
| export | {format: 'csv'|'pdf'|'json', filter: dict, row_count: int} |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Mapping Audit row count is lower than the number of committed records. | An older migration ran before the mfi_migration_audit module was installed. | Run the back-fill job at Settings -> Technical -> Migration -> Backfill Audit. It walks production records with x_migration_run_id set and creates missing audit rows. Marks them with backfilled=True for transparency. |
| Hash chain verification report flags row 4,217 as broken. | A database admin edited the audit row directly via psql. | Investigate via BridgeERP logs and DB audit; never accept an edited row as legitimate. SOC 2 control failure requires an incident response. |
| Cannot find lineage on a member who I am sure was migrated. | Member was matched to an existing partner during Commit rather than created new (match-by-PIN). Lineage is on the matched partner, not a new one. | Search by source_key on Mapping Audit, not by res.partner.name. |
| Exporting PDF for SASRA fails for filters returning more than 50,000 rows. | PDF generation is single-threaded and capped to protect the worker. | Split the filter or use the CSV export and convert externally; or queue the PDF as a background job (Settings -> Technical -> Migration -> Queued PDF Export). |

