This document explains the GovStack Government-to-Person (G2P) bulk disbursement architecture as implemented in mifos-gazelle, based on the official GovStack specification and the actual codebase implementation.
Key References:
- GovStack Spec:
/home/tdaly/my-mac-dir/tmp/bulk-disburesement.pdf - Implementation: Payment Hub EE (PHEE) components in this repository
- Architecture Overview
- Understanding Payment Modes and Tenants
- How GovStack Mode Works
- Payer Account Configuration
- Component Details
- Tenant Configuration
- Workflow Comparison
- How to Run G2P Successfully
- Troubleshooting
According to the official GovStack spec (pages 9-13), the architecture for G2P bulk disbursement is:
┌────────────────────────────────────────────────────────┐
│ GOVERNMENT ENTITY (e.g., Social Welfare Ministry) │
│ - Treasury Single Account (TSA) │
│ - Registration Building Block (beneficiary lists) │
└──────────────────┬─────────────────────────────────────┘
│
│ (2) Bulk Payment Batch
│ (RegisteringInstID, ProgramID, CSV)
▼
┌────────────────────────────────────────────────────────┐
│ PAYMENTS BUILDING BLOCK (Payment Hub) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Account Mapper (Identity Account Mapper) │ │
│ │ - Pre-validates beneficiaries │ │
│ │ - Identifies payee FSPs │ │
│ │ - Returns bankingInstitutionCode │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Bulk Processor │ │
│ │ - De-bulks by receiving institution │ │
│ │ - Creates sub-batches per payee FSP │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────┬─────────────────────────────────────┘
│
│ (3) De-bulked Sub-batches
│ (grouped by Payee FSP)
▼
┌────────────────────────────────────────────────────────┐
│ PAYER FSP (Payer Bank - e.g., greenbank/redbank) │
│ - Holds government settlement account │
│ - Participant in payment switch/scheme │
└──────────────────┬─────────────────────────────────────┘
│
│ (4) Clearing Instructions
│ (per scheme rules)
▼
┌────────────────────────────────────────────────────────┐
│ PAYMENT SWITCH / SCHEME (Switch-Agnostic) │
│ - Routes to destination FSPs │
│ - Handles settlement │
│ - Could be: Mojaloop vNext, National Switch, │
│ Bilateral, or Direct (closedloop) │
│ - GSMA: not yet implemented in Mifos Gazelle v2.0.0 │
└──────────────────┬─────────────────────────────────────┘
│
│ (5) Individual Credits
┌────────┴────────┬────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Payee │ │ Payee │ │ Payee │
│ FSP 1 │ │ FSP 2 │ │ FSP 3 │
│(bluebank)│ │(redbank) │ │(momo) │
└──────────┘ └──────────┘ └──────────┘
Key Principle from GovStack Spec (Page 12-13):
"Payments Building Block does not interface directly with the Payment Switch. Payments Building Block interfaces with the Switch/Scheme through a Participant of the Switch/Scheme. The Payer forwards all instructions to the Scheme/Switch."
CRITICAL: The GovStack spec does NOT mandate Mojaloop - it is switch-agnostic. The spec only requires:
- Identity/Account Mapper for validation
- Batch de-bulking by payee FSP
- Payer FSP as intermediary to switch
- Switch type is implementation-specific
In mifos-gazelle, we configure three tenant roles:
| Tenant | Role | Database | Use Case |
|---|---|---|---|
| greenbank | Payer (Mojaloop) | greenbank schema | Government/payer using Mojaloop switch |
| redbank | Payer (Closedloop) | redbank schema | Government/payer using direct transfers |
| bluebank | Payee FSP | bluebank schema | Beneficiary financial institution |
Key Point: The tenant you specify when submitting a batch (--tenant) determines which payer workflows are used, not the switch type.
The payment_mode column in the batch CSV determines the routing mechanism:
| Payment Mode | Routing | Switch Involved? | Use Case |
|---|---|---|---|
CLOSEDLOOP |
Direct connector-bulk → connector-channel | NO | Internal transfers, same Payment Hub instance |
MOJALOOP |
Via Mojaloop vNext switch | YES | Inter-FSP transfers via Mojaloop |
GSMA |
Via GSMA mobile money connector | Depends | Mobile money providers — not yet implemented in Mifos Gazelle v2.0.0 / PHEE mifos-v2.0.0 |
Critical Understanding:
CLOSEDLOOPis a routing method, NOT the same as "non-GovStack"CLOSEDLOOPcan be used WITH GovStack identity validationMOJALOOPcan be used WITHOUT GovStack identity validation- These are independent concerns
Location: ph-ee-connector-bulk/.../BatchTransferWorker.java:123-139
if("closedloop".equalsIgnoreCase(paymentMode)){
// Direct HTTP call to connector-channel
boolean success = processClosedloopTransfers(transactionList, batchId, debulkingDfspId);
}
else{
// Create recursive batch back to bulk-processor (e.g., for Mojaloop)
String batchId = invokeBatchTransactionApi(fileName, updatedCsvData, ...);
}Important: There is no govstack.enabled configuration flag. "GovStack mode" is triggered at runtime by the presence of HTTP headers in the batch submission request. The --govstack flag in submit-batch.py simply causes those headers to be sent.
The triggering header:
X-Registering-Institution-ID: greenbank
When this header is present, the bulk-processor:
- Uses the
bulk_processor_account_lookup-{tenant}BPMN workflow (configured inbpmns.tenants) - Calls identity-account-mapper to validate all beneficiaries
- De-bulks the batch by
bankingInstitutionCode(payee FSP) - Looks up the payer account from
budget-accountYAML config (ifX-Program-IDalso present)
Without this header, the bulk_processor-{tenant} workflow runs and payer/payee values come from the CSV.
Code reference: ph-ee-bulk-processor/.../ProcessorStartRouteService.java:168-173
if (!(StringUtils.hasText(registeringInstituteId) && StringUtils.hasText(programId))) {
// Headers missing — use CSV payer values as-is
exchange.setProperty(IS_UPDATED, false);
return;
}
// Headers present — look up payer from budget-account configurationBoth workflows are deployed at startup and selected at runtime:
| Workflow | Process ID | Triggered When | Key Tasks |
|---|---|---|---|
| Standard | bulk_processor-{tenant} |
No GovStack headers | partyLookup, deduplicate |
| GovStack | bulk_processor_account_lookup-{tenant} |
X-Registering-Institution-ID present |
batchAccountLookup, batchAccountLookupCallback |
File locations:
- Standard:
orchestration/feel/bulk_processor-DFSPID.bpmn - GovStack:
orchestration/feel/bulk_processor_account_lookup-DFSPID.bpmn
Configuring which workflow a tenant uses (in ph-ee-bulk-processor/src/main/resources/application.yaml):
GovStack mode uses a separate key batch-transactions-govstack — the standard batch-transactions key is not overwritten:
bpmns:
tenants:
- id: "greenbank"
flows:
payment-transfer: "minimal_mock_fund_transfer-{dfspid}"
batch-transactions: "bulk_processor-{dfspid}" # Standard (no --govstack)
batch-transactions-govstack: "bulk_processor_account_lookup-{dfspid}" # GovStack (--govstack)Both keys are already present in the current configuration — no change is needed to enable GovStack mode for greenbank or redbank.
| Aspect | Payer (Government/Program) | Payee (Beneficiary/Citizen) |
|---|---|---|
| Source | budget-account config in application.yaml (GovStack mode) OR CSV columns (standard mode) |
identity_account_mapper database |
| Lookup Key | X-Registering-Institution-ID + X-Program-ID headers |
payeeIdentity + registeringInstitutionId |
| Auto-discoverable? | No — must be in CSV or configured | Yes — MSISDN → account via mapper |
| Changes require restart? | Yes (config change → JAR rebuild) | No (live DB updates) |
identity-account-mapper is used ONLY for payee (beneficiary) lookups, never for payer.
--govstack |
payment_mode |
--tenant |
Bulk Workflow | Payment Workflow | Use Case |
|---|---|---|---|---|---|
| NO | CLOSEDLOOP | redbank | bulk_processor | minimal_mock_fund_transfer | Simple testing |
| NO | MOJALOOP | greenbank | bulk_processor | PayerFundTransfer | Multi-FSP via switch |
| YES | CLOSEDLOOP | redbank | bulk_processor_account_lookup | minimal_mock_fund_transfer | G2P validation, no switch |
| YES | MOJALOOP | greenbank | bulk_processor_account_lookup | PayerFundTransfer | TRUE GOVSTACK — recommended |
In GovStack mode with X-Program-ID header, the payer bank account is looked up from a static YAML configuration (not the database). This removes the need for payer details in the CSV.
File: ph-ee-bulk-processor/src/main/resources/application.yaml
budget-account:
registeringInstitutions:
- id: "greenbank" # matches X-Registering-Institution-ID header
programs:
- id: "SocialWelfare" # matches X-Program-ID header
name: "Social Welfare"
identifierType: "MSISDN" # MSISDN, ACCOUNT, etc.
identifierValue: "0413509790" # Payer phone number (MSISDN) for greenbankHeader → config mapping:
| HTTP Header | Config Field | Effect |
|---|---|---|
X-Registering-Institution-ID: greenbank |
registeringInstitutions[].id |
Selects institution |
X-Program-ID: SocialWelfare |
programs[].id |
Selects program within institution |
When both headers match, identifierValue (e.g., "0413509790") is used as the payer identifier. Any payer columns in the CSV are overwritten with this value.
Multiple programs example:
budget-account:
registeringInstitutions:
- id: "greenbank"
programs:
- id: "SocialWelfare"
name: "Social Welfare"
identifierType: "MSISDN"
identifierValue: "0413509790" # Payer phone number for SocialWelfare program
- id: "ChildBenefit"
name: "Child Benefit Program"
identifierType: "MSISDN"
identifierValue: "0413509791" # Different payer phone numberThe current configuration uses identifierType: "MSISDN", so identifierValue is the payer's mobile number. To find or change it:
Via MifosX web client:
- Open
https://mifos.mifos.gazelle.test, select tenantgreenbank - Navigate to the government program client → view their mobile number
Via database:
kubectl exec -n infra mysql-0 -- mysql -umifos -ppassword \
-D mifostenant-greenbank \
-e "SELECT id, mobile_no, display_name FROM m_client LIMIT 10;"The CSV format is the same in both standard and GovStack modes — payer columns are always required. The Platform-TenantId header determines the payer's FSP; X-Program-ID (if supplied) overwrites the payer identifier with the value from budget-account config.
id,request_id,payment_mode,payer_identifier_type,payer_identifier,payee_identifier_type,payee_identifier,amount,currency,note
0,uuid1,mojaloop,MSISDN,0413509790,MSISDN,0495822412,250,USD,Sept welfare
1,uuid2,mojaloop,MSISDN,0413509790,MSISDN,0495822413,250,USD,Sept welfareSee src/utils/batch/bulk-gazelle-mojaloop-4.csv and src/utils/batch/bulk-gazelle-closedloop-4.csv for working examples.
Purpose (GovStack spec page 7):
"The account mapper service identifies the FSP, and exact destination address where the payee's account is used to route payouts to beneficiaries."
Database: identity_account_mapper
API: POST /api/v1/identity-account-mapper/batch-account-lookup
Request:
{
"requestID": "batch-001",
"registeringInstitutionID": "greenbank",
"beneficiaries": [
{
"payeeIdentity": "0495822412",
"paymentModality": "00"
}
]
}Response (to callback URL):
{
"requestID": "batch-001",
"registeringInstitutionID": "greenbank",
"beneficiaries": [
{
"payeeIdentity": "0495822412",
"paymentModality": "00",
"financialAddress": "000000001",
"bankingInstitutionCode": "bluebank"
}
]
}bankingInstitutionCodeidentifies which FSP serves this beneficiary (used for de-bulking)financialAddressis for reconciliation, not party lookup
Location: ph-ee-bulk-processor/.../SplittingRoute.java:74-109
How it works:
- Original batch: 10 transactions to 3 different FSPs
- After de-bulking: 3 sub-batches (1 per FSP), each processed independently
- Grouping key:
bankingInstitutionCodefrom identity-account-mapper response
Enabled when both isPartyLookupEnabled and isBatchAccountLookupEnabled are true in the GovStack workflow.
GovStack does NOT mandate Mojaloop - any switch/scheme can be used.
Oracle Registration:
MSISDN: 0495822412 → fspId: "bluebank", currency: "USD"
How Routing Works:
- Payer FSP sends transfer request with MSISDN to switch
- Switch queries oracle: "Which FSP owns this MSISDN?"
- Oracle responds with FSP ID
- Switch routes transfer to that FSP's callback URL
Location: ph-ee-bulk-processor/src/main/resources/application.yaml
payment-modes:
- id: "CLOSEDLOOP"
type: "BULK"
endpoint: "bulk_connector_{MODE}-{dfspid}"
# debulkingDfspid: "greenbank" # ❌ DO NOT hardcode- When
debulkingDfspidis null, the submitting tenant is used (correct behavior) - Hardcoding this causes ALL closedloop batches to use the same tenant workflows
Code reference: ph-ee-bulk-processor/.../InitSubBatchRoute.java:128
variables.put(DEBULKINGDFSPID,
mapping.getDebulkingDfspid() == null ? tenantName : mapping.getDebulkingDfspid());For standard (non-hostPath) deployments, tenant configuration is managed entirely through .properties files in the Gazelle Helm chart. These are bundled into a Kubernetes ConfigMap (ph-ee-config) at deploy time and injected as Spring Boot configuration into each component.
Key files:
| File | Controls |
|---|---|
repos/ph_template/helm/gazelle/config/application-tenants.properties |
BPMN workflow mappings per tenant (bulk-processor and connector-channel) |
repos/ph_template/helm/gazelle/config/application-tenantsConnection.properties |
Per-tenant MySQL database connections for operations-app |
The ConfigMap template at repos/ph_template/helm/gazelle/templates/config.yml bundles all config/*.properties files automatically:
data:
{{ (.Files.Glob "config/**.properties").AsConfig | nindent 2 }}application-tenants.properties — this is the primary file to edit for tenant workflow configuration:
# Greenbank — Payer using Mojaloop switch
bpmns.tenants[0].id=greenbank
bpmns.tenants[0].flows.payment-transfer=PayerFundTransfer-{dfspid}
bpmns.tenants[0].flows.outbound-transfer-request=minimal_mock_transfer_request-{dfspid}
bpmns.tenants[0].flows.batch-transactions=bulk_processor-{dfspid}
bpmns.tenants[0].flows.batch-transactions-govstack=bulk_processor_account_lookup-{dfspid}
# Redbank — Payer using closedloop
bpmns.tenants[1].id=redbank
bpmns.tenants[1].flows.payment-transfer=minimal_mock_fund_transfer-{dfspid}
bpmns.tenants[1].flows.outbound-transfer-request=minimal_mock_transfer_request-{dfspid}
bpmns.tenants[1].flows.batch-transactions=bulk_processor-{dfspid}
bpmns.tenants[1].flows.batch-transactions-govstack=bulk_processor_account_lookup-{dfspid}
# Bluebank — Payee FSP
bpmns.tenants[2].id=bluebank
bpmns.tenants[2].flows.payment-transfer=minimal_mock_fund_transfer-{dfspid}
bpmns.tenants[2].flows.batch-transactions=bulk_processor-{dfspid}To add a new tenant or change a workflow mapping, edit this file and redeploy:
helm upgrade phee repos/ph_template/helm/gazelle -n paymenthub -f config/ph_values.yamlNote: When using hostPath mounts for local development, the ConfigMap has no effect — changes must be made to the source YAML files and the JAR rebuilt. See Applying Configuration Changes below.
When developing with hostPath mounts, the same configuration must be set in two separate source YAML files. The properties file above corresponds to these YAML equivalents:
File: ph-ee-bulk-processor/src/main/resources/application.yaml
Uses @ConfigurationProperties — reads YAML, NOT .properties files.
# Tenant list
tenants: "greenbank, bluebank, redbank"
# Workflow mapping per tenant
bpmns:
tenants:
- id: "greenbank"
flows:
payment-transfer: "minimal_mock_fund_transfer-{dfspid}"
batch-transactions: "bulk_processor-{dfspid}" # Standard
batch-transactions-govstack: "bulk_processor_account_lookup-{dfspid}" # GovStack
- id: "greenbank-mastercard"
flows:
payment-transfer: "MastercardFundTransfer-{dfspid}"
batch-transactions: "bulk_processor_account_lookup-{dfspid}"
- id: "redbank"
flows:
payment-transfer: "minimal_mock_fund_transfer-{dfspid}"
batch-transactions: "bulk_processor-{dfspid}" # Standard
batch-transactions-govstack: "bulk_processor_account_lookup-{dfspid}" # GovStack
- id: "bluebank"
flows:
batch-transactions: "bulk_processor-{dfspid}"File: ph-ee-connector-channel/src/main/resources/application.yml
bpmns:
tenants:
- id: "greenbank"
flows:
payment-transfer: "PayerFundTransfer-{dfspid}"
outbound-transfer-request: "{ps}_flow_{ams}-{dfspid}"
- id: "redbank"
flows:
payment-transfer: "minimal_mock_fund_transfer-{dfspid}"
outbound-transfer-request: "minimal_mock_transfer_request-{dfspid}"
- id: "bluebank"
flows:
payment-transfer: "minimal_mock_fund_transfer-{dfspid}"
outbound-transfer-request: "minimal_mock_transfer_request-{dfspid}"Key Point: The payment-transfer workflow is determined by the payer tenant, not the payment_mode in the CSV.
Workflow selection logic: ph-ee-connector-channel/.../ChannelRouteBuilder.java:304-367
String tenantId = exchange.getIn().getHeader("Platform-TenantId", String.class);
// tenantId="greenbank" → "PayerFundTransfer-greenbank"
// tenantId="redbank" → "minimal_mock_fund_transfer-redbank"See Primary Configuration: Helm Chart Properties Files above. For standard deployments, repos/ph_template/helm/gazelle/config/application-tenants.properties is the single file to edit — no JAR rebuild required.
# 1. Edit source YAML files
nano ~/ph-ee-bulk-processor/src/main/resources/application.yaml
nano ~/ph-ee-connector-channel/src/main/resources/application.yml
# 2. Rebuild JARs
cd ~/ph-ee-bulk-processor && ./gradlew clean build -x test
cd ~/ph-ee-connector-channel && ./gradlew clean build -x test
# 3. Restart pods
kubectl delete pod -n paymenthub -l app=ph-ee-bulk-processor
kubectl delete pod -n paymenthub -l app=ph-ee-connector-channelConfigMap updates alone do NOT work with hostPath mounts.
Used by: greenbank tenant
Flow:
- Payee User Lookup — Query switch for payee FSP
- Local Quote — Calculate payer FSP quote
- Payee Quote — Get quote from payee FSP via switch
- Payer Block Funds — Reserve funds in payer account
- Send transfer request — POST to Mojaloop switch
/transfers - Payer Book Funds — Commit/rollback based on response
Used by: redbank tenant (payer), bluebank tenant (payee)
Flow:
- mockPayeeLookup — Simulate party lookup
- mockInitiateTransfer — Simulate transfer (no actual Fineract call)
- mockPayeeAccountStatus — Return success
Purpose: Development/testing without full Fineract integration.
./src/utils/batch/submit-batch.py \
[-c ~/my-config.ini] \ # optional; defaults to config/config.ini
-f <csv-file> \
--tenant <greenbank|redbank|bluebank> \
[--govstack] \
[--registering-institution <id>] \ # auto-detected from CSV if omitted
[--program <program-id>] \ # sends X-Program-ID for budget-account lookup
[--debug] # shows BPMN workflow and payment modes before submitDecision table:
| Use case | --tenant |
--govstack |
payment_mode |
|---|---|---|---|
| Simple internal test | redbank | NO | CLOSEDLOOP |
| Multi-FSP via Mojaloop | greenbank | NO | MOJALOOP |
| G2P bulk disbursement (recommended) | greenbank | YES | MOJALOOP |
| G2P closedloop (same PH instance only) | redbank | YES | CLOSEDLOOP |
# Generate CSVs from current Mifos client data
./src/utils/data-loading/generate-example-csv-files.py
# Closedloop — redbank payer, no identity validation
./src/utils/batch/submit-batch.py \
-f ./src/utils/batch/bulk-gazelle-closedloop-4.csv \
--tenant redbank
# Mojaloop — greenbank payer, no identity validation
./src/utils/batch/submit-batch.py \
-f ./src/utils/batch/bulk-gazelle-mojaloop-4.csv \
--tenant greenbankPrerequisites:
# Ensure identity-account-mapper has beneficiaries
./src/utils/data-loading/generate-mifos-vnext-data.py --regenerate
# Verify registrations
kubectl exec -n infra mysql-0 -- mysql -umifos -ppassword identity_account_mapper -e \
"SELECT id.payee_identity, pmd.institution_code
FROM identity_details id
JOIN payment_modality_details pmd ON id.payment_modality_id = pmd.id
WHERE id.registering_institution_id = 'greenbank'
LIMIT 5"No configuration change needed — greenbank already has batch-transactions-govstack: "bulk_processor_account_lookup-{dfspid}" in ~/ph-ee-bulk-processor/src/main/resources/application.yaml. The --govstack flag in submit-batch.py automatically routes to this workflow via the X-Registering-Institution-ID header.
Submit:
# --registering-institution is auto-detected from CSV payees
./src/utils/batch/submit-batch.py \
-f ./src/utils/batch/bulk-gazelle-mojaloop-4.csv \
--tenant greenbank \
--govstack
# With budget-account payer lookup (requires budget-account config in application.yaml)
./src/utils/batch/submit-batch.py \
-f ./src/utils/batch/bulk-gazelle-mojaloop-4.csv \
--tenant greenbank \
--govstack \
--program SocialWelfare
# Debug mode — shows workflow name, payment modes, institution detection
./src/utils/batch/submit-batch.py \
-f ./src/utils/batch/bulk-gazelle-mojaloop-4.csv \
--tenant greenbank \
--govstack \
--debugExpected Flow:
--registering-institutionauto-detected from CSV payee MSISDNs via identity-account-mapper DB- Identity mapper validates all beneficiaries
- Returns
bankingInstitutionCodefor each (e.g., "bluebank") - Batch de-bulked by payee FSP
- Sub-batches sent to greenbank's PayerFundTransfer workflow
- Transfers go via Mojaloop switch
- Switch routes to destination FSPs
No configuration change needed — redbank already has batch-transactions-govstack configured.
./src/utils/batch/submit-batch.py \
-f ./src/utils/batch/bulk-gazelle-closedloop-4.csv \
--tenant redbank \
--govstackLimitations: All beneficiaries must be in the same Payment Hub instance. De-bulking occurs but all sub-batches go to the same system.
# Check batch status
kubectl exec -n paymenthub operationsmysql-0 -- mysql -uroot -pmysql operations_app -e \
"SELECT batch_id, total, successful, failed, ongoing FROM batch ORDER BY id DESC LIMIT 3"
# Check transfer details with FSP mapping
kubectl exec -n paymenthub operationsmysql-0 -- mysql -uroot -pmysql operations_app -e \
"SELECT id, batch_id, payee_identifier, payee_dfsp_id, status
FROM transfers ORDER BY id DESC LIMIT 5"
# Check bulk-processor logs for de-bulking
kubectl logs -n paymenthub -l app=ph-ee-bulk-processor --tail=100 | grep -i splitting
# Check which workflow was used
kubectl logs -n paymenthub -l app=ph-ee-connector-channel --tail=100 | grep -i "starting workflow"
# Verify identity mapper was called (--govstack submissions)
kubectl logs -n paymenthub -l app=ph-ee-identity-account-mapper --tail=50
# Verify identity mapper has entries for your institution
kubectl exec -n infra mysql-0 -- mysql -umifos -ppassword identity_account_mapper -e \
"SELECT COUNT(*) FROM identity_details WHERE registering_institution_id = 'greenbank'"| Symptom | Likely Cause | Solution |
|---|---|---|
| Batch total = 0 | Identity mapper empty for this institution | Run generate-mifos-vnext-data.py --regenerate or submit without --govstack |
| All transactions same FSP | Missing vNext oracle registration | Run generate-mifos-vnext-data.py --regenerate |
| No sub-batches created | Using bulk_processor instead of bulk_processor_account_lookup workflow |
Check batch-transactions config in bulk-processor application.yaml |
| Wrong workflow triggered | Wrong --tenant |
Verify tenant matches your CSV (redbank for closedloop, greenbank for mojaloop) |
| Auto-detection fails | Payees not in identity-account-mapper | Pass --registering-institution explicitly; check mapper has data |
| "No registering institution found for id: X" | budget-account.registeringInstitutions.id doesn't match header |
Check YAML config matches X-Registering-Institution-ID value exactly |
| Symptom | Likely Cause | Solution |
|---|---|---|
| "Process definition not found" (412 error) | Tenant missing from bpmns.tenants[] in bulk-processor |
Add tenant to ~/ph-ee-bulk-processor/src/main/resources/application.yaml, rebuild JAR |
| PARTY_NOT_FOUND errors | Tenant missing from channel-connector config | Add tenant to ~/ph-ee-connector-channel/src/main/resources/application.yml, rebuild JAR |
| Wrong workflow triggered despite correct tenant | Hardcoded debulkingDfspid in payment-mode config |
Remove debulkingDfspid from CLOSEDLOOP payment-mode in bulk-processor application.yaml |
| Changes not taking effect | Using hostPath mounts | Rebuild JAR files and restart pods after config changes |
| Payer account wrong after config change | Pod not restarted | kubectl delete pod -n paymenthub -l app=ph-ee-bulk-processor |
# Check which clients are in each tenant's Fineract
curl -s -u mifos:password -H "Fineract-Platform-TenantId: greenbank" \
"http://mifos.mifos.gazelle.localhost/fineract-provider/api/v1/clients?limit=1" | \
jq '.pageItems[0].mobileNo'
# List Fineract savings accounts for payer account lookup
kubectl exec -n infra mysql-0 -- mysql -umifos -ppassword \
-D mifostenant-greenbank \
-e "SELECT id, account_no, display_name FROM m_savings_account LIMIT 10;"-
GovStack Spec is Switch-Agnostic
- Does NOT mandate Mojaloop
- Requires: identity validation, de-bulking, payer FSP intermediary
- Switch type is implementation choice
-
GovStack "mode" is header-driven, not a config flag
X-Registering-Institution-IDheader triggers the GovStack workflowX-Program-IDheader additionally enables budget-account payer lookup--govstackinsubmit-batch.pysends these headers
-
Three Tenant Roles:
- greenbank = Mojaloop payer — connector-channel uses
PayerFundTransfer(routes via vNext switch); bulk-processor usesminimal_mock_fund_transferfor individual transfer simulation - redbank = Closedloop payer — both components use
minimal_mock_fund_transfer - bluebank = Payee FSP (receives funds)
- greenbank = Mojaloop payer — connector-channel uses
-
Payer and Payee have different sources:
- Payer →
budget-accountconfig in application.yaml (GovStack with X-Program-ID) OR CSV columns - Payee → identity-account-mapper database (always)
- Payer →
-
Three independent controls:
--govstack= enables identity validation and batch de-bulkingpayment_modein CSV = routing method (CLOSEDLOOP vs MOJALOOP)--tenant= which payer workflows to use
-
--registering-institutionis now auto-detected- submit-batch.py queries the identity-account-mapper DB to find the institution
- Override with
--registering-institutionif needed
# Standard closedloop testing
./src/utils/data-loading/generate-example-csv-files.py
./src/utils/batch/submit-batch.py -f ./src/utils/batch/bulk-gazelle-closedloop-4.csv --tenant redbank
# True GovStack G2P via Mojaloop (registering institution auto-detected)
./src/utils/batch/submit-batch.py -f ./src/utils/batch/bulk-gazelle-mojaloop-4.csv --tenant greenbank --govstack
# GovStack with explicit program/payer account config
./src/utils/batch/submit-batch.py -f ./src/utils/batch/bulk-gazelle-mojaloop-4.csv --tenant greenbank --govstack --program SocialWelfare
# Debug: see which BPMN workflow will fire before submitting
./src/utils/batch/submit-batch.py -f ./src/utils/batch/bulk-gazelle-mojaloop-4.csv --tenant greenbank --govstack --debug- Bulk-processor workflows + budget-account:
ph-ee-bulk-processor/src/main/resources/application.yaml - Connector-channel tenants:
ph-ee-connector-channel/src/main/resources/application.yml - Helm template tenants:
repos/ph_template/helm/gazelle/config/application-tenants.properties - Connector-mojaloop switch config:
ph-ee-connector-mojaloop-java/src/main/resources/application.yml:40
- GovStack batch:
orchestration/feel/bulk_processor_account_lookup-DFSPID.bpmn - Standard batch:
orchestration/feel/bulk_processor-DFSPID.bpmn - Mojaloop transfer:
repos/phlabs/orchestration/feel/PayerFundTransfer-DFSPID.bpmn - Closedloop transfer:
orchestration/feel/minimal_mock_fund_transfer-DFSPID.bpmn
- GovStack header processing + payer config lookup:
ph-ee-bulk-processor/.../ProcessorStartRouteService.java:140-213 - Budget-account config classes:
ph-ee-bulk-processor/.../config/BudgetAccountConfig.java - Batch de-bulking:
ph-ee-bulk-processor/.../SplittingRoute.java:74-109 - Payment mode routing:
ph-ee-connector-bulk/.../BatchTransferWorker.java:123-139 - Workflow selection (channel):
ph-ee-connector-channel/.../ChannelRouteBuilder.java:304-367 - Mojaloop switch call:
ph-ee-connector-mojaloop-java/.../TransferRoutes.java:214-216
- CSV generator:
src/utils/data-loading/generate-example-csv-files.py - Batch submitter:
src/utils/batch/submit-batch.py - Bulk CSV files:
src/utils/batch/bulk-gazelle-*.csv - Data generator:
src/utils/data-loading/generate-mifos-vnext-data.py
Last Updated: March 2026