Banking-grade ledger auditing, reconciliation, double-entry accounting, and South African regulatory compliance skill for the MyMoolah Treasury Platform (MMTP). Enforces FICA Act 38/2001, POPIA, SARB/FSCA prudential standards, Mojaloop FSPIOP, SOX-grade internal controls, SHA-256 hash-chained immutable audit trails, automated N-way reconciliation, and structured PASS/WARN/FAIL audit workflows. Use this skill whenever working on ledger accounts, journal entries, reconciliation, settlement, float management, compliance reporting, or any financial data integrity task.
You are an expert financial systems auditor specializing in South African digital payment platforms, mobile money, and Mojaloop-compliant interoperable payment schemes. When this skill is active, you MUST enforce banking-grade standards for every financial operation, ledger modification, reconciliation process, compliance check, and audit trail in the MyMoolah Treasury Platform.
This skill activates when the context involves:
MyMoolah Account Code Convention: The codebase uses hierarchical codes like
2100-01-01 (User Wallet ZAR), 1200-10-04 (Flash Float), 4100-10-01
(Commission Revenue). The 5-digit Mojaloop-aligned codes below (10100, 20100)
are reference standards -- the actual codes in ledger_accounts may differ.
Always query the LedgerAccount model for real codes before hardcoding.
Mojaloop Reference → MMTP Actual Code Mapping:
| Mojaloop Ref | MMTP Code | Name |
|---|---|---|
| 10100 | 1100-01-01 | Standard Bank Current Account |
| 10200 | 1200-10-04 / 1200-10-05 | Supplier Float (Flash / MobileMart) |
| 20100 | 2100-01-01 | User Wallet ZAR |
| 20500 | 2600-01-01 | Unallocated Suspense |
| 40100 | 4100-10-01 | Commission Revenue — Airtime/Data |
| 40200 | 4100-10-02 | Commission Revenue — Electricity |
| 50100 | 5100-01-01 | Supplier COGS |
Every financial event MUST produce a balanced journal entry where:
Σ debits = Σ credits (for every JournalEntry)
NEVER allow a journal entry to be persisted if debits ≠ credits.
MyMoolah uses LedgerAccount with code, name, type, and normalSide fields.
Required Account Types:
| Type | Normal Side | Purpose |
|---|---|---|
ASSET | Debit | Bank accounts, float holdings, receivables |
LIABILITY | Credit | User wallet balances, payables, deposits held |
EQUITY | Credit | Owner's equity, retained earnings |
REVENUE | Credit | Commission income, fee income, ad revenue |
EXPENSE | Debit | Transaction costs, supplier fees, operational costs |
CONTRA | Varies | Contra-accounts (e.g., allowance for bad debts) |
Account Code Convention (Mojaloop-aligned):
1xxxx = Assets (e.g., 10100 = Settlement Bank Account)
2xxxx = Liabilities (e.g., 20100 = User Wallet Balances)
3xxxx = Equity (e.g., 30100 = Retained Earnings)
4xxxx = Revenue (e.g., 40100 = Commission Income)
5xxxx = Expenses (e.g., 50100 = Transaction Processing Fees)
Canonical Reference: See
docs/CHART_OF_ACCOUNTS.mdfor the complete, authoritative Chart of Accounts with all 28+ accounts, journal templates, solvency rules, reserved ranges, and product registration checklist. That document supersedes any account listings in this skill.
10100 - Settlement Bank Account (Standard Bank) [ASSET/Debit]
10200 - Float Holding Account [ASSET/Debit]
10300 - Receivables - Suppliers [ASSET/Debit]
10400 - Receivables - Merchants [ASSET/Debit]
10500 - USDC Custody Account [ASSET/Debit]
10600 - Prefunding Account [ASSET/Debit]
10700 - EasyPay Clearing Account [ASSET/Debit]
20100 - User Wallet Balances (aggregate liability) [LIABILITY/Credit]
20200 - Merchant Float Balances [LIABILITY/Credit]
20300 - Payables - Suppliers [LIABILITY/Credit]
20400 - Payables - Settlement [LIABILITY/Credit]
20500 - Suspense Account [LIABILITY/Credit]
20600 - Unallocated Funds [LIABILITY/Credit]
30100 - Retained Earnings [EQUITY/Credit]
30200 - Owner's Equity [EQUITY/Credit]
40100 - Commission Income - Airtime/Data [REVENUE/Credit]
40200 - Commission Income - Electricity [REVENUE/Credit]
40300 - Commission Income - Other VAS [REVENUE/Credit]
40400 - Ad Revenue - Watch-to-Earn [REVENUE/Credit]
40500 - Transaction Fee Income [REVENUE/Credit]
40600 - Interchange Income [REVENUE/Credit]
50100 - Transaction Processing Fees [EXPENSE/Debit]
50200 - Supplier Settlement Costs [EXPENSE/Debit]
50300 - Banking & Payment Gateway Fees [EXPENSE/Debit]
50400 - Ad Campaign Costs [EXPENSE/Debit]
50500 - Reward Payouts [EXPENSE/Debit]
When creating journal entries:
reference field for idempotency.sequelize.transaction().Σ debits - Σ credits and
reject if ≠ 0.DECIMAL(18,2) for ZAR,
DECIMAL(18,6) for USDC. NEVER use floating-point arithmetic for money.reference field on JournalEntry ensures
idempotency -- the same business event cannot be double-posted.Example: User Wallet Deposit via EasyPay
await sequelize.transaction(async (t) => {
const entry = await JournalEntry.create({
reference: `DEP-${transactionId}`,
description: `EasyPay deposit for user ${userId}`,
postedAt: new Date()
}, { transaction: t });
await JournalLine.bulkCreate([
{ entryId: entry.id, accountId: settlementBankAccId, dc: 'debit', amount: depositAmount, memo: 'EasyPay deposit received' },
{ entryId: entry.id, accountId: userWalletAccId, dc: 'credit', amount: depositAmount, memo: `Wallet credit for user ${userId}` }
], { transaction: t });
const lines = await JournalLine.findAll({ where: { entryId: entry.id }, transaction: t });
const totalDebit = lines.filter(l => l.dc === 'debit').reduce((s, l) => s + parseFloat(l.amount), 0);
const totalCredit = lines.filter(l => l.dc === 'credit').reduce((s, l) => s + parseFloat(l.amount), 0);
if (Math.abs(totalDebit - totalCredit) > 0.001) {
throw new Error(`LEDGER IMBALANCE: debit=${totalDebit}, credit=${totalCredit}`);
}
});
The existing ReconRun model tracks: total_transactions, matched_exact,
matched_fuzzy, unmatched_mmtp, unmatched_supplier, amount_variance.
Mandatory Steps for Every Recon Run:
file_hash field) to ensure integrity
and prevent duplicate processing.ml_anomalies JSONB field)amount_variance = total_amount_mmtp - total_amount_supplier
commission_variance = total_commission_mmtp - total_commission_supplier
matchRate >= 99.0% AND |variance| <= thresholdMISSING_IN_MMTP -- supplier has record, MyMoolah doesn'tMISSING_IN_SUPPLIER -- MyMoolah has record, supplier doesn'tAMOUNT_MISMATCH -- both have record, amounts differSTATUS_MISMATCH -- transaction status disagreementTIMING_DIFFERENCE -- settlement date disagreementReconAuditTrail.| Data Source | Frequency | SLA |
|---|---|---|
| Flash/Ringo (airtime) | Daily | T+1 |
| MobileMart (data/airtime/electricity/billers) | Daily | T+1 |
| EasyPay deposits | Real-time + daily batch | T+0 |
| Standard Bank settlement (MT940/MT942) | Daily | T+1 |
| USDC on-chain | Per-block confirmation | T+0 |
| Merchant float top-ups | Daily | T+1 |
| Ad revenue/Watch-to-Earn | Weekly | T+7 |
| Commission/VAT | Monthly | T+30 |
Adopted from openclaw/skills Agent Audit Trail (MIT-0) and enhanced for banking-grade requirements. Every significant financial event is recorded with a SHA-256 chain linking entries -- making tampering detectable and providing an authoritative compliance record for FICA, POPIA, and SARB.
Core Invariants:
ReconAuditTrail has NO updatedAt -- records are
immutable once written. DELETE is prohibited at the application layer.event_hash (SHA-256) and
previous_event_hash, creating a tamper-evident chain.ord) ensuring
strict chronological ordering within each audit domain.actor_type (system/user/admin),
actor_id, ip_address, and user_agent.Each audit trail entry conforms to this schema:
{
"event_id": "uuid-v4",
"event_timestamp": "2026-04-03T21:00:00.000+02:00",
"kind": "JOURNAL_ENTRY_POSTED",
"actor_type": "system",
"actor_id": "productPurchaseService",
"domain": "ledger",
"plane": "action",
"ord": 42,
"target": "journal_entries:DEP-txn-abc123",
"summary": "Double-entry journal posted: R50.00 EasyPay deposit",
"metadata": { "transactionId": "abc123", "amount": "50.00", "currency": "ZAR" },
"ip_address": "10.0.0.1",
"user_agent": "MMTP/2.72.0",
"previous_event_hash":"sha256:abc123def456...",
"event_hash": "sha256:789ghi012jkl..."
}
Field Reference:
| Field | Type | Description |
|---|---|---|
event_id | UUID v4 | Globally unique event identifier |
event_timestamp | ISO-8601 | Timestamp with SAST offset (+02:00) |
kind | string | Event type (see 3.3) |
actor_type | enum | system, user, admin, scheduler, migration |
actor_id | string | Service name, user ID, or admin ID |
domain | string | Audit domain: ledger, recon, wallet, kyc, fica, settlement |
plane | string | ingress, decision, action, compliance |
ord | integer | Monotonically increasing sequence per domain |
target | string | Resource affected (table:reference or endpoint) |
summary | string | Human-readable description |
metadata | JSONB | Structured event-specific data (amounts, IDs, before/after) |
ip_address | string | Source IP (redacted for PII compliance where needed) |
user_agent | string | Client or service identifier |
previous_event_hash | string | SHA-256 of the preceding entry in this domain |
event_hash | string | SHA-256 of this entry (excluding the hash field itself) |
LEDGER_ACCOUNT_CREATED — New account added to Chart of Accounts
LEDGER_ACCOUNT_DEACTIVATED — Account soft-deleted
JOURNAL_ENTRY_POSTED — Double-entry journal posted
JOURNAL_ENTRY_REVERSED — Correcting reversal posted
RECON_RUN_STARTED — Reconciliation initiated
RECON_RUN_COMPLETED — Reconciliation finished with results
RECON_MATCH_CONFIRMED — Transaction pair matched
RECON_DISCREPANCY_FLAGGED — Unresolved discrepancy identified
RECON_DISCREPANCY_RESOLVED — Manual resolution applied
FLOAT_ADJUSTMENT — Float balance adjusted
SETTLEMENT_INITIATED — Settlement batch started
SETTLEMENT_COMPLETED — Settlement batch finalized
BALANCE_SNAPSHOT — Periodic balance state capture
SUSPICIOUS_ACTIVITY_DETECTED — ML anomaly or threshold breach
USER_WALLET_CREDITED — Money credited to user wallet
USER_WALLET_DEBITED — Money debited from user wallet
KYC_STATUS_CHANGED — Know Your Customer status update
FICA_CDD_COMPLETED — Customer Due Diligence performed
FICA_EDD_TRIGGERED — Enhanced Due Diligence required
FICA_SAR_FILED — Suspicious Activity Report submitted to FIC
FICA_CTR_FILED — Cash Threshold Report submitted
POPIA_DATA_ACCESS_REQUEST — Data subject access request received
POPIA_DATA_DELETION_REQUEST — Data subject deletion request received
POPIA_BREACH_DETECTED — Security compromise identified
POPIA_BREACH_NOTIFIED — Information Regulator and subjects notified
CREDENTIAL_ACCESSED — Secret or API key accessed by service
PERMISSION_CHANGED — User role or access level modified
COMMISSION_CALCULATED — Commission/VAT computed for transaction
SUPPLIER_FAILOVER — Circuit breaker triggered supplier switch
const crypto = require('crypto');
function computeEventHash(entry) {
const payload = {
event_id: entry.event_id,
event_timestamp: entry.event_timestamp,
kind: entry.kind,
actor_type: entry.actor_type,
actor_id: entry.actor_id,
domain: entry.domain,
ord: entry.ord,
target: entry.target,
summary: entry.summary,
metadata: entry.metadata,
previous_event_hash: entry.previous_event_hash
};
const raw = JSON.stringify(payload, Object.keys(payload).sort());
return 'sha256:' + crypto.createHash('sha256').update(raw).digest('hex');
}
async function verifyAuditChain(domain, startDate, endDate) {
const events = await ReconAuditTrail.findAll({
where: {
domain,
event_timestamp: { [Op.between]: [startDate, endDate] }
},
order: [['ord', 'ASC']]
});
const results = { valid: true, brokenAt: null, totalEvents: events.length, verified: 0 };
for (let i = 0; i < events.length; i++) {
const computed = computeEventHash(events[i]);
if (computed !== events[i].event_hash) {
return { ...results, valid: false, brokenAt: i, error: 'SELF_HASH_MISMATCH' };
}
if (i > 0 && events[i].previous_event_hash !== events[i - 1].event_hash) {
return { ...results, valid: false, brokenAt: i, error: 'CHAIN_LINK_BROKEN' };
}
results.verified++;
}
return results;
}
-- Prevent any UPDATE or DELETE on the audit trail
CREATE OR REPLACE FUNCTION prevent_audit_mutation()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'AUDIT TRAIL VIOLATION: % operations are prohibited on recon_audit_trail',
TG_OP;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER enforce_worm_audit_trail
BEFORE UPDATE OR DELETE ON recon_audit_trail
FOR EACH ROW EXECUTE FUNCTION prevent_audit_mutation();
Run periodically (at minimum daily) to verify the fundamental accounting equation:
Σ(Asset debits) + Σ(Expense debits) = Σ(Liability credits) + Σ(Equity credits) + Σ(Revenue credits)
async function verifyTrialBalance() {
const accounts = await LedgerAccount.findAll({ where: { isActive: true } });
let totalDebits = 0;
let totalCredits = 0;
for (const account of accounts) {
const lines = await JournalLine.findAll({ where: { accountId: account.id } });
const debits = lines.filter(l => l.dc === 'debit').reduce((s, l) => s + parseFloat(l.amount), 0);
const credits = lines.filter(l => l.dc === 'credit').reduce((s, l) => s + parseFloat(l.amount), 0);
if (account.normalSide === 'debit') {
totalDebits += (debits - credits);
} else {
totalCredits += (credits - debits);
}
}
const balanced = Math.abs(totalDebits - totalCredits) < 0.01;
return {
balanced,
totalDebits,
totalCredits,
variance: totalDebits - totalCredits
};
}
For every supplier/merchant, the float balance in the wallet system MUST equal the corresponding ledger account balance:
Σ(Float credits) - Σ(Float debits) = Float balance in MerchantFloat/ClientFloat model
The total of all individual user wallet balances MUST equal the 20100 - User Wallet Balances ledger account:
Σ(individual user balances from Wallet table) = Net balance of account 20100
Settlement Bank Account ledger balance MUST reconcile against the actual bank statement (MT940/MT942):
10100 net balance = Bank statement closing balance + outstanding deposits - outstanding withdrawals
Σ(commission journal lines for period) = Σ(commission_amount in MyMoolahTransaction for period)
Σ(VAT journal lines for period) = Σ(commission * 0.15) for VAT-applicable transactions
Commission Configuration Reference:
config/supplier-commissions.json
(no longer hardcoded in if/else chains)supplier_commission_tiers tableproduct_selection_rules tablev_best_offers materialized view resolves the winning supplier/product
for each VAS category, auto-refreshed after every catalog syncKnown Issue — tax_transactions FK Constraint:
The tax_transactions.originalTransactionId FK can fail when the parent
transactions row is not yet committed at the time the tax record is inserted.
Commission journal entries are still posted correctly; only the
tax_transactions audit record fails to persist. This is tracked in the
tech debt register.
For each DFSP/supplier, maintain a running net position:
Net Position = Σ(outgoing transfers) - Σ(incoming transfers) - liquidity cover
Before processing any transaction, verify:
Available liquidity = NDC (Net Debit Cap) - current_position
if (transfer_amount > available_liquidity) → REJECT
This section codifies the legal obligations that MMTP must satisfy as an accountable institution / payment service provider operating in South Africa. Every agent working on MyMoolah financial code MUST understand and enforce these requirements. Non-compliance carries criminal penalties, licence revocation, and fines up to R10 million per infringement.
The FIC Act is South Africa's primary anti-money-laundering (AML) and counter-terrorism financing (CTF) legislation. MyMoolah is an accountable institution under Schedule 1.
Before establishing a business relationship or processing a single transaction:
Audit requirement: Every CDD event MUST produce a FICA_CDD_COMPLETED audit
trail entry with metadata containing: verification method, risk rating, document
types collected, verifier identity.
For higher-risk clients (PEPs, high-value, unusual patterns):
Audit requirement: FICA_EDD_TRIGGERED audit event with rationale and
approver identity.
All CDD documentation and transaction records MUST be retained for:
Records include:
Cash transactions above R24,999.99 MUST be reported to the FIC within the prescribed period. For digital wallets:
Audit requirement: FICA_CTR_FILED audit event with transaction references,
aggregate amount, and submission confirmation.
Any person who knows or ought reasonably to have known that funds may be proceeds of unlawful activity MUST report to the FIC:
Audit requirement: FICA_SAR_FILED audit event. The SAR itself is
confidential -- NEVER expose SAR filing status to the client (tipping off
is a criminal offence under Section 29(4)).
MMTP MUST maintain an RMCP that includes:
POPIA is South Africa's comprehensive data protection legislation, comparable to GDPR. MyMoolah is a "responsible party" processing personal information of data subjects (wallet users).
Personal information may only be processed if:
For financial data, FICA record-keeping requirements provide the lawful basis for retaining transaction data. Consent is required for marketing or optional data collection.
Personal information must be collected for a specific, explicitly defined, and lawful purpose. MMTP collects personal information for:
Data collected for one purpose MUST NOT be repurposed without fresh consent or a new lawful basis.
Collect only the personal information that is adequate, relevant, and not excessive for the stated purpose. For MMTP:
Personal information must not be retained longer than necessary:
| Data Type | Retention Period | Legal Basis |
|---|---|---|
| Transaction records | 5 years post-relationship | FICA s.22 |
| KYC documents | 5 years post-relationship | FICA s.22 |
| Journal entries | 7 years | Companies Act / SOX-grade |
| Audit trail events | 7 years | Immutable, encrypted |
| User profile data | Duration of relationship + 5 years | FICA s.22 |
| Marketing consent | Until withdrawal | POPIA s.11(3) |
| Support chat logs | 2 years | Legitimate interest |
After the retention period, personal information MUST be de-identified or securely destroyed. Automated retention policies should be implemented.
The responsible party MUST:
For MMTP, this translates to:
When there are reasonable grounds to believe personal information has been accessed or acquired by an unauthorised person:
Audit requirement: POPIA_BREACH_DETECTED followed by POPIA_BREACH_NOTIFIED
audit events with full incident metadata.
MMTP must support the following data subject requests:
Audit requirement: POPIA_DATA_ACCESS_REQUEST and POPIA_DATA_DELETION_REQUEST
audit events for every data subject request.
The SARB oversees payment system stability and monetary policy. Relevant frameworks for MMTP:
The FSCA regulates market conduct and financial service providers:
| Report | Frequency | Regulator |
|---|---|---|
| Client fund safeguarding | Monthly | SARB/PA |
| AML/CTF compliance report | Quarterly | FIC |
| Transaction volumes and values | Monthly | SARB |
| Capital adequacy (if required) | Quarterly | PA |
| Complaints register | Quarterly | FSCA |
| Annual financial statements | Annually | FSCA/CIPC |
| External audit report | Annually | FSCA |
| RMCP effectiveness review | Annually | FIC |
Mobile number verification requirements for SIM-based wallet services. All user phone numbers must be RICA-verified before wallet activation.
MMTP financial records should be prepared in accordance with IFRS for SMEs (or full IFRS if required by turnover/asset thresholds):
2300-10-01 must reconcile to SARS VAT201 returns| Standard | Scope | MyMoolah Applicability |
|---|---|---|
| FICA Act 38/2001 | AML/CTF | CDD, EDD, SAR, CTR, RMCP |
| POPIA Act 4/2013 | Data protection | All personal information processing |
| NPS Act 78/1998 | Payment systems | Wallet, transfers, settlement |
| SARB Directives | Payment regulation | PSP licensing, fund safeguarding |
| FSCA Conduct Standards | Market conduct | Consumer protection, complaints |
| FAIS Act 37/2002 | Financial services | If advisory services offered |
| Companies Act 71/2008 | Corporate governance | Financial reporting, retention |
| RICA | Communications | SIM/phone verification |
| IFRS / IAS | Financial reporting | Ledger presentation standards |
| Mojaloop FSPIOP | Interoperability | Payment scheme compliance |
| PCI-DSS | Card data | If card data is handled |
| ISO 20022 | Messaging | RTP and payment messaging |
| SOX (voluntary) | Internal controls | Audit trail, segregation of duties |
While SOX (Sarbanes-Oxley) is US legislation, MMTP voluntarily adopts SOX-grade internal controls as a banking-grade best practice. These patterns align with SARB prudential expectations and COSO framework requirements.
| Component | MMTP Implementation |
|---|---|
| Control Environment | Board-approved policies, compliance officer, code of conduct |
| Risk Assessment | RMCP, transaction monitoring, ML anomaly detection |
| Control Activities | Segregation of duties, approval workflows, access controls |
| Information & Communication | Audit trail, structured logging, incident reporting |
| Monitoring | Automated health checks, periodic audits, hash chain verification |
| Function | Cannot Also Perform |
|---|---|
| Transaction creation | Transaction approval |
| Journal entry posting | Journal entry reversal approval |
| Reconciliation execution | Discrepancy resolution |
| User onboarding (KYC) | KYC status approval |
| Ledger account creation | Account deactivation |
| Migration authoring | Migration deployment to production |
| Float top-up initiation | Float top-up confirmation |
| SAR preparation | SAR filing decision |
| Code deployment | Production database access |
A material weakness exists when there is a reasonable possibility that a material misstatement will not be prevented or detected on a timely basis.
Auto-detected material weaknesses:
| Condition | Severity | Response |
|---|---|---|
| Trial balance variance > R1.00 | CRITICAL | Halt all transactions, page on-call |
| Wallet aggregate ≠ ledger account 20100 | CRITICAL | Halt deposits/withdrawals |
| Audit chain broken (hash mismatch) | CRITICAL | Forensic investigation required |
| Recon match rate < 95% for any supplier | HIGH | Escalate to finance team |
| Journal entry without reference (idempotency gap) | HIGH | Block and review |
| Unresolved discrepancy > 48 hours | MEDIUM | Auto-escalate to management |
| Float balance < supplier minimum threshold | MEDIUM | Alert for top-up |
| SAR not filed within 24 hours of detection | CRITICAL | Compliance violation |
| CDD records missing for active client | HIGH | Suspend account pending CDD |
All audit trail entries, journal entries, and reconciliation results MUST be stored in Write Once Read Many (WORM) mode:
| Change Type | Required Controls |
|---|---|
| Schema migration | Peer review + admin approval + rollback script |
| Production deployment | CI/CD pipeline + Cloud Build (no manual deploys) |
| Environment variable change | Documented in deployment script + audit trail |
| Access permission change | Dual authorization + audit trail event |
| Financial configuration change | Board/management approval + versioned config |
Adopted from CFO Stack /cfo-audit pattern and enhanced for banking-grade requirements. This workflow is the standard for all periodic audits.
[ ] Trial balance is balanced (Σ debits = Σ credits, variance < R0.01)
[ ] All ledger accounts have correct type and normalSide
[ ] No orphaned journal lines (missing parent JournalEntry)
[ ] No journal entries with unbalanced lines
[ ] All balance assertions hold
[ ] Database schema matches expected migration state
[ ] Every day in the audit period has transactions (no unexplained gaps)
[ ] All bank accounts have daily balance snapshots
[ ] All expected revenue sources have transactions
[ ] All VAS suppliers have reconciliation runs for every business day
[ ] Commission and VAT journal entries exist for every purchase transaction
[ ] Referral earnings have matching REFERRAL-% journal entries
[ ] Float top-ups have matching journal entries
[ ] No duplicate transactions (same date + amount + reference + payee)
[ ] No transactions with missing or null amounts
[ ] All wallet balances >= 0 (no negative wallet balances)
[ ] User wallet aggregate matches ledger account 20100
[ ] Float balances match corresponding ledger accounts
[ ] Commission amounts match supplier commission configuration
[ ] VAT calculated at exactly 15% on VAT-applicable commissions
[ ] EasyPay deposit amounts match bank statement credits
[ ] RTP (Request to Pay) amounts match Standard Bank credits
[ ] No unusually large transactions (> 3x typical for category)
[ ] No transactions outside business hours that seem unlikely
[ ] No round-number transactions that might be estimates (> R10,000)
[ ] No duplicate references across different journal entries
[ ] No wallet balance changes without corresponding journal entries
[ ] Supplier failover events reviewed (circuit breaker activations)
[ ] Velocity checks: no user exceeding transaction limits
[ ] FICA: All active users have completed CDD (no CDD gaps)
[ ] FICA: Cash threshold reports filed for aggregate deposits > R24,999.99
[ ] FICA: Suspicious activity detected and reported within 24 hours
[ ] POPIA: No personal information in application logs
[ ] POPIA: Data subject requests processed within 30-day SLA
[ ] POPIA: Retention policies enforced (no data held beyond period)
[ ] SARB: Transaction volumes reported as required
[ ] Audit trail: Hash chain integrity verified for all domains
[ ] Audit trail: No gaps in monotonic ordering
[ ] Segregation of duties: No SoD violations in the period
AUDIT REPORT — MyMoolah Treasury Platform
Period: [start_date] to [end_date]
Generated: [timestamp] by [auditor]
═══════════════════════════════════════════════════════
STRUCTURAL: [PASS|WARN|FAIL] ([n] accounts, [m] journal entries)
COMPLETENESS: [PASS|WARN|FAIL] ([x]/[y] days covered, [z]% coverage)
ACCURACY: [PASS|WARN|FAIL] ([n] checks passed, [m] warnings)
ANOMALIES: [PASS|WARN|FAIL] ([n] flagged for review)
COMPLIANCE: [PASS|WARN|FAIL] ([n] FICA/POPIA checks passed)
AUDIT TRAIL: [PASS|WARN|FAIL] ([n] events verified, chain intact)
OVERALL: [PASS|WARN|FAIL] with [n] warning(s) and [m] action(s) needed
CRITICAL FINDINGS:
1. [Finding with specific transaction/entry reference]
WARNINGS:
1. [Warning with specific reference]
ACTIONS NEEDED:
1. [Action with deadline and responsible party]
| Score | Criteria |
|---|---|
| PASS | All checks pass, no findings |
| WARN | Minor findings that don't affect financial integrity (cosmetic, timing) |
| FAIL | Any finding that affects financial integrity, compliance, or data accuracy |
Automatic FAIL conditions (non-negotiable):
[ ] Trial balance is balanced (debits = credits)
[ ] No journal entries with imbalanced lines
[ ] No orphaned journal lines (missing parent entry)
[ ] User wallet aggregate = Ledger account 20100
[ ] No transactions in indeterminate state > 15 minutes
[ ] No negative wallet balances
[ ] Audit trail hash chain valid for last 100 events per domain
[ ] Float balances match ledger accounts
[ ] All scheduled recon runs completed
[ ] Recon match rate >= 99% for all suppliers
[ ] Amount variance within threshold for all suppliers
[ ] Audit trail chain integrity verified (full day)
[ ] No unresolved discrepancies > 24 hours old
[ ] Commission/VAT journal entries match transaction records
[ ] FICA CTR threshold monitoring (aggregate daily cash deposits)
[ ] No PII detected in application log files
[ ] Bank statement reconciliation complete (MT940/MT942)
[ ] Commission/fee income reconciled across all suppliers
[ ] Aging analysis of unmatched transactions
[ ] ML anomaly review and false-positive tuning
[ ] Audit trail hash chain full verification (all domains)
[ ] Supplier float balance adequacy check
[ ] POPIA data subject request SLA compliance
[ ] Full trial balance report generated and archived
[ ] SARB transaction volume reporting prepared
[ ] FICA RMCP effectiveness review
[ ] Segregation of duties compliance verification
[ ] Data retention policy enforcement (purge expired data)
[ ] Penetration test / vulnerability scan results reviewed
[ ] Backup and disaster recovery test
[ ] Full external audit support package prepared
[ ] FICA RMCP annual review and update
[ ] POPIA impact assessment review
[ ] Insurance adequacy review
[ ] Business continuity plan test
[ ] Complete audit trail archive to cold storage (GCS WORM)
MMTP uses Google Cloud Scheduler (not node-cron) for automated tasks on Cloud Run, because Cloud Run kills idle instances mid-sweep. Scheduled endpoints use OIDC authentication.
Scheduled Audit-Related Jobs:
| Job | Schedule | Endpoint | Timeout |
|---|---|---|---|
| VAS catalog sync + view refresh | Daily 02:00 SAST | POST /api/v1/catalog/scheduled-sync | 1800s |
| Referral payout processing | Daily 02:15 SAST | POST /api/v1/referrals/scheduled-payout | 300s |
| Reconciliation runs | Per-supplier schedule | Supplier-specific endpoints | 600s |
Pattern for New Scheduled Audits:
// 1. Create authenticated endpoint
router.post('/api/v1/audit/scheduled-health-check',
requireCloudSchedulerAuth, // OIDC token validation
async (req, res) => {
const results = await runHealthChecks();
// Log results to audit trail
await logAuditEvent('SCHEDULED_HEALTH_CHECK', results);
res.json({ success: true, results });
}
);
// 2. Create Cloud Scheduler job via gcloud
// gcloud scheduler jobs create http mmtp-daily-health-check \
// --schedule="0 3 * * *" \
// --uri="https://api-mm.mymoolah.africa/api/v1/audit/scheduled-health-check" \
// --http-method=POST \
// --oidc-service-account-email=cloud-scheduler@mmtp-*.iam.gserviceaccount.com \
// --oidc-audience="https://api-mm.mymoolah.africa" \
// --time-zone="Africa/Johannesburg" \
// --attempt-deadline=1800s
When a transaction fails mid-way, create a compensating (reversal) journal entry:
async function createReversalEntry(originalRef, reason) {
const original = await JournalEntry.findOne({
where: { reference: originalRef },
include: [{ model: JournalLine, as: 'lines' }]
});
if (!original) throw new Error(`Original entry ${originalRef} not found`);
return sequelize.transaction(async (t) => {
const reversal = await JournalEntry.create({
reference: `REV-${originalRef}`,
description: `Reversal: ${reason}`,
postedAt: new Date()
}, { transaction: t });
const reversedLines = original.lines.map(line => ({
entryId: reversal.id,
accountId: line.accountId,
dc: line.dc === 'debit' ? 'credit' : 'debit',
amount: line.amount,
memo: `Reversal of ${originalRef}: ${reason}`
}));
await JournalLine.bulkCreate(reversedLines, { transaction: t });
return reversal;
});
}
When a transaction cannot be immediately classified:
20500 - Suspense AccountSUSPENSE_ENTRY_CREATED| Severity | Response Time | Notification | Examples |
|---|---|---|---|
| P1 - CRITICAL | Immediate (< 15 min) | CTO + Compliance Officer | Ledger imbalance, data breach, SAR deadline |
| P2 - HIGH | < 1 hour | Engineering Lead | Recon failure, float depletion, SoD violation |
| P3 - MEDIUM | < 4 hours | On-call engineer | Supplier failover, anomaly detected |
| P4 - LOW | Next business day | Team channel | Cosmetic discrepancy, timing difference |
When reviewing any PR that touches financial code, verify:
DECIMAL, never FLOAT or DOUBLEsequelize.transaction()reference field is set for idempotency on journal entriesDecimal library, not JS floats| Model | File | Purpose |
|---|---|---|
LedgerAccount | models/LedgerAccount.js | Chart of Accounts |
JournalEntry | models/JournalEntry.js | Double-entry journal headers |
JournalLine | models/JournalLine.js | Debit/credit lines per entry |
ReconRun | models/ReconRun.js | Reconciliation run metadata |
ReconTransactionMatch | models/ReconTransactionMatch.js | Individual transaction matches |
ReconAuditTrail | models/ReconAuditTrail.js | Immutable audit log with hash chaining |
ReconSupplierConfig | models/ReconSupplierConfig.js | Supplier reconciliation settings |
MerchantFloat | models/MerchantFloat.js | Merchant float balances |
ClientFloat | models/ClientFloat.js | Client float balances |
MyMoolahTransaction | models/MyMoolahTransaction.js | Transaction records |
FlashTransaction | models/FlashTransaction.js | VAS provider transactions |
Settlement | models/Settlement*.js | Settlement processing |
Wallet | models/Wallet.js | User wallet balances (SELECT FOR UPDATE) |
v_best_offers | Materialized view (SQL) | Commission-based supplier selection for VAS products; auto-refreshed after catalog sync |
ProductVariant | models/ProductVariant.js | Normalized VAS product variants with supplier pricing |
ProductSelectionRule | product_selection_rules table | Rules for selecting winning supplier per VAS category |
┌─────────────────────────────┐
│ South African Regulators │
│ (FIC, SARB, FSCA, InfoReg) │
└──────────┬──────────────────┘
│ Reporting
│ (SAR, CTR, Volumes)
┌────────────────────┐│
│ External Providers ││
│ (EasyPay, Flash, ││
│ MobileMart, SBSA)││
└─────────┬──────────┘│
│ │
┌───────▼───────┐ │
│ Integration │───┤── Reconciliation Engine
│ Layer │ │ (ReconRun + N-Way Matching)
└───────┬───────┘ │
│ │
┌────────────▼────────────┐
│ Double-Entry Ledger │
│ ┌──────────────────┐ │
│ │ JournalEntry │ │
│ │ └─ JournalLine │ │
│ │ └─ Account │ │
│ └──────────────────┘ │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ Immutable Audit Trail │
│ (SHA-256 Hash Chain) │
│ ┌───────────────────┐ │
│ │ WORM Storage │ │
│ │ Monotonic Order │ │
│ │ Actor Tracking │ │
│ │ FICA/POPIA Events │ │
│ └───────────────────┘ │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ Compliance & Controls │
│ ┌───────────────────┐ │
│ │ FICA CDD/EDD/SAR │ │
│ │ POPIA Safeguards │ │
│ │ SoD Enforcement │ │
│ │ Material Weakness │ │
│ │ Health Checks │ │
│ └───────────────────┘ │
└─────────────────────────┘
SELECT
SUM(CASE WHEN jl.dc = 'debit' THEN jl.amount ELSE 0 END) as total_debits,
SUM(CASE WHEN jl.dc = 'credit' THEN jl.amount ELSE 0 END) as total_credits,
SUM(CASE WHEN jl.dc = 'debit' THEN jl.amount ELSE 0 END) -
SUM(CASE WHEN jl.dc = 'credit' THEN jl.amount ELSE 0 END) as net_balance
FROM journal_lines jl;
-- Expected: net_balance = 0.00
SELECT
la.code, la.name, la.type, la."normalSide",
SUM(CASE WHEN jl.dc = 'debit' THEN jl.amount ELSE 0 END) as debits,
SUM(CASE WHEN jl.dc = 'credit' THEN jl.amount ELSE 0 END) as credits,
CASE WHEN la."normalSide" = 'debit'
THEN SUM(CASE WHEN jl.dc = 'debit' THEN jl.amount ELSE -jl.amount END)
ELSE SUM(CASE WHEN jl.dc = 'credit' THEN jl.amount ELSE -jl.amount END)
END as balance
FROM ledger_accounts la
LEFT JOIN journal_lines jl ON la.id = jl."accountId"
WHERE la."isActive" = true
GROUP BY la.id, la.code, la.name, la.type, la."normalSide"
ORDER BY la.code;
SELECT
a1.event_id,
a1.event_hash,
a1.previous_event_hash,
a2.event_hash as expected_previous,
CASE WHEN a1.previous_event_hash = a2.event_hash THEN 'VALID' ELSE 'BROKEN' END as chain_status
FROM recon_audit_trail a1
LEFT JOIN recon_audit_trail a2 ON a2.event_timestamp = (
SELECT MAX(event_timestamp) FROM recon_audit_trail
WHERE event_timestamp < a1.event_timestamp AND run_id = a1.run_id
)
WHERE a1.run_id = :runId
ORDER BY a1.event_timestamp;
SELECT
t."userId",
u."phoneNumber",
DATE(t."createdAt") as txn_date,
COUNT(*) as deposit_count,
SUM(t.amount) as total_deposits
FROM "MyMoolahTransactions" t
JOIN users u ON u.id = t."userId"
WHERE t.type = 'deposit'
AND t."createdAt" >= CURRENT_DATE
AND t.status = 'completed'
GROUP BY t."userId", u."phoneNumber", DATE(t."createdAt")
HAVING SUM(t.amount) > 24999.99
ORDER BY total_deposits DESC;
-- Clients exceeding R24,999.99 aggregate daily deposits require CTR filing
# Detect potential PII leaks in application logs
# Run weekly as part of POPIA Section 19 safeguard verification
rg -i '(0[6-8][0-9]{8}|27[6-8][0-9]{8}|\b[A-Z]{2}[0-9]{6,13}\b)' logs/ \
--glob '*.log' --count
# Expected: 0 matches (all PII should be redacted)
SELECT
(SELECT SUM(balance) FROM wallets WHERE balance > 0) as wallet_sum,
(SELECT
SUM(CASE WHEN jl.dc = 'credit' THEN jl.amount ELSE 0 END) -
SUM(CASE WHEN jl.dc = 'debit' THEN jl.amount ELSE 0 END)
FROM journal_lines jl
JOIN ledger_accounts la ON la.id = jl."accountId"
WHERE la.code LIKE '2100%'
) as ledger_balance,
(SELECT SUM(balance) FROM wallets WHERE balance > 0) -
(SELECT
SUM(CASE WHEN jl.dc = 'credit' THEN jl.amount ELSE 0 END) -
SUM(CASE WHEN jl.dc = 'debit' THEN jl.amount ELSE 0 END)
FROM journal_lines jl
JOIN ledger_accounts la ON la.id = jl."accountId"
WHERE la.code LIKE '2100%'
) as variance;
-- Expected: variance = 0.00
MyMoolah has a comprehensive production audit script at scripts/production-full-audit.js
that implements many of the checks described in this skill. Run it with:
node scripts/production-full-audit.js --uat # UAT environment
node scripts/production-full-audit.js --staging # Staging environment
node scripts/production-full-audit.js --production # Production environment
The script performs:
All agents should be familiar with this script and its output format.
This skill is optimized for Claude Opus 4.6 with extended thinking in Cursor IDE.
When running audits, use this pattern for maximum accuracy:
For full platform audits, delegate to parallel subagents:
| Subagent | Scope | Files to Read |
|---|---|---|
| Structural | Trial balance, orphaned entries | scripts/production-full-audit.js |
| Compliance | FICA/POPIA checks | services/kycService.js, controllers/kycController.js |
| Reconciliation | Supplier match rates | services/reconService.js, models/ReconRun.js |
| Commission | VAT/commission accuracy | services/commissionVatService.js, config/supplier-commissions.json |
# Full audit (all checks)
node scripts/production-full-audit.js --production
# Quick ledger balance check
node scripts/production-full-audit.js --uat 2>&1 | head -30
# Verify specific account balance
node -e "const {getUATClient}=require('./scripts/db-connection-helper'); (async()=>{const c=await getUATClient(); const r=await c.query(\"SELECT code, name FROM ledger_accounts WHERE code LIKE '2100%'\"); console.table(r.rows); c.release();})()"
This skill should be read by the agent when the task involves ANY of:
production-full-audit.js