feat(fundtransfer+imps+mfa): fund transfer module, IMPS switch client, MFA log API

- Fund transfer: NEFT/RTGS/IMPS/internal strategies, limit resolution, bulk
  transfer, verification tokens, suspense GL, recall/approval flow, jobs
- IMPS CBS switch client with crypto (JWT/envelope) and transaction domain
- MFA log read API (audit of MFA events)
- Liquibase migrations 0300-0311 for fund transfer / IMPS tables, permissions,
  roles, and suspense account
- JobName, SecurityConfig, dependencies.gradle wiring

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
Aditya
2026-06-01 12:18:30 +05:30
parent 6413befe07
commit ae7017ca81
102 changed files with 30046 additions and 114506 deletions
@@ -327,4 +327,16 @@
<include file="parts/0018_cash_reconciliation.xml" relativeToChangelogFile="true"/>
<include file="parts/0019_cash_reversal_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0020_cash_permissions.xml" relativeToChangelogFile="true"/>
<include file="parts/0300_create_m_imps_transaction.xml" relativeToChangelogFile="true"/>
<include file="parts/0301_create_m_bank_ifsc.xml" relativeToChangelogFile="true"/>
<include file="parts/0302_create_m_transfer_limit_config.xml" relativeToChangelogFile="true"/>
<include file="parts/0303_create_m_customer_transfer_limit.xml" relativeToChangelogFile="true"/>
<include file="parts/0304_create_m_fund_transfer.xml" relativeToChangelogFile="true"/>
<include file="parts/0305_create_m_fund_transfer_event.xml" relativeToChangelogFile="true"/>
<include file="parts/0306_create_m_verification_token.xml" relativeToChangelogFile="true"/>
<include file="parts/0307_seed_fund_transfer_permissions.xml" relativeToChangelogFile="true"/>
<include file="parts/0308_grant_fund_transfer_permissions_to_roles.xml" relativeToChangelogFile="true"/>
<include file="parts/0309_phase3_jobs_and_recall_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0310_fund_transfer_roles_and_token_cleanup_job.xml" relativeToChangelogFile="true"/>
<include file="parts/0311_create_fund_transfer_suspense_account.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
One row per inbound IMPS request received by Fineract acting as the CBS
under IMPS_BANK_API_SPEC_CBS_1.0 §6. Used for audit + idempotency: a
retried (api, txn_id) replays the prior recorded response.
-->
<changeSet id="0300_create_m_imps_transaction" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_imps_transaction"/></not></preConditions>
<createTable tableName="m_imps_transaction">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<!-- REQ_PAY / REQ_VAL_ADD / REQ_CHK_TXN -->
<column name="api" type="VARCHAR(32)">
<constraints nullable="false"/>
</column>
<column name="txn_id" type="VARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="org_txn_id" type="VARCHAR(64)"/>
<column name="ref_id" type="VARCHAR(64)"/>
<column name="utr" type="VARCHAR(64)"/>
<column name="amount" type="VARCHAR(32)"/>
<column name="payee_account_number" type="VARCHAR(64)"/>
<column name="payee_ifsc" type="VARCHAR(32)"/>
<!-- SUCCESS / DEEMED / FAILED -->
<column name="status" type="VARCHAR(16)"/>
<!-- 00 / 91 / XC / 8 / etc. -->
<column name="response_code" type="VARCHAR(8)"/>
<column name="request_json" type="CLOB"/>
<column name="response_json" type="CLOB"/>
<!-- Standard Fineract audit columns (AbstractAuditableWithUTCDateTimeCustom). -->
<column name="created_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="version" type="BIGINT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
</createTable>
<!-- Idempotency: a retried (api, txn_id) resolves to the prior row. -->
<addUniqueConstraint tableName="m_imps_transaction" columnNames="api, txn_id"
constraintName="uq_imps_txn_api_txnid"/>
<!-- Common audit queries: latest activity per payee account. -->
<createIndex tableName="m_imps_transaction" indexName="idx_imps_txn_payee_account">
<column name="payee_account_number"/>
</createIndex>
<!-- Reconciliation by UTR. -->
<createIndex tableName="m_imps_transaction" indexName="idx_imps_txn_utr">
<column name="utr"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
OUR bank's IFSC codes. Used to classify a transfer as ON_US (payee.ifsc matches
a row here) vs OFF_US (route via switch). The real RBI/sponsor-bank list is
bulk-loaded at deployment time via tenant data-import — the placeholder row
below just keeps a non-empty set for dev.
-->
<changeSet id="0301_create_m_bank_ifsc" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_bank_ifsc"/></not></preConditions>
<createTable tableName="m_bank_ifsc">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="ifsc_code" type="VARCHAR(11)">
<constraints nullable="false" unique="true" uniqueConstraintName="uq_bank_ifsc_code"/>
</column>
<column name="branch_code" type="VARCHAR(16)"/>
<column name="branch_name" type="VARCHAR(255)"/>
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="version" type="BIGINT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet id="0301_seed_placeholder_bank_ifsc" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_bank_ifsc</sqlCheck>
</preConditions>
<insert tableName="m_bank_ifsc">
<column name="ifsc_code" value="BANK0000001"/>
<column name="branch_code" value="MAIN"/>
<column name="branch_name" value="Head Office (placeholder — replace with sponsor-bank IFSC list before go-live)"/>
<column name="is_active" valueBoolean="true"/>
<column name="created_by" valueNumeric="1"/>
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
<column name="last_modified_by" valueNumeric="1"/>
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Global per-channel transfer limits + maker-checker thresholds. Per-customer
overrides live in m_customer_transfer_limit. Resolution = customer override
first, fall back to this table.
maker_checker_above semantics:
NULL or 0 → every transfer needs maker-checker
huge val → never (auto-execute)
else → maker-checker required when amount >= threshold
-->
<changeSet id="0302_create_m_transfer_limit_config" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_transfer_limit_config"/></not></preConditions>
<createTable tableName="m_transfer_limit_config">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="channel" type="VARCHAR(16)">
<constraints nullable="false"/>
</column>
<column name="direction" type="VARCHAR(8)"/>
<column name="per_txn_min" type="DECIMAL(19,4)" defaultValueNumeric="1.00"/>
<column name="per_txn_max" type="DECIMAL(19,4)"/>
<column name="per_day_max" type="DECIMAL(19,4)"/>
<column name="maker_checker_above" type="DECIMAL(19,4)"/>
<column name="is_active" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="version" type="BIGINT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
</createTable>
<addUniqueConstraint tableName="m_transfer_limit_config" columnNames="channel"
constraintName="uq_transfer_limit_config_channel"/>
</changeSet>
<changeSet id="0302_seed_transfer_limit_config_defaults" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_transfer_limit_config</sqlCheck>
</preConditions>
<!-- INTERNAL (on-us): no upper cap, maker-checker at ₹50,000 -->
<insert tableName="m_transfer_limit_config">
<column name="channel" value="INTERNAL"/>
<column name="per_txn_min" valueNumeric="1.00"/>
<column name="maker_checker_above" valueNumeric="50000.00"/>
<column name="is_active" valueBoolean="true"/>
<column name="created_by" valueNumeric="1"/>
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
<column name="last_modified_by" valueNumeric="1"/>
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
</insert>
<!-- IMPS: per RBI, ₹5 lakh per-txn cap; maker-checker at ₹1 lakh -->
<insert tableName="m_transfer_limit_config">
<column name="channel" value="IMPS"/>
<column name="per_txn_min" valueNumeric="1.00"/>
<column name="per_txn_max" valueNumeric="500000.00"/>
<column name="per_day_max" valueNumeric="1000000.00"/>
<column name="maker_checker_above" valueNumeric="100000.00"/>
<column name="is_active" valueBoolean="true"/>
<column name="created_by" valueNumeric="1"/>
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
<column name="last_modified_by" valueNumeric="1"/>
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
</insert>
<!-- NEFT: no cap, maker-checker at ₹2 lakh -->
<insert tableName="m_transfer_limit_config">
<column name="channel" value="NEFT"/>
<column name="per_txn_min" valueNumeric="1.00"/>
<column name="maker_checker_above" valueNumeric="200000.00"/>
<column name="is_active" valueBoolean="true"/>
<column name="created_by" valueNumeric="1"/>
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
<column name="last_modified_by" valueNumeric="1"/>
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
</insert>
<!-- RTGS: min ₹2 lakh, maker-checker always -->
<insert tableName="m_transfer_limit_config">
<column name="channel" value="RTGS"/>
<column name="per_txn_min" valueNumeric="200000.00"/>
<column name="maker_checker_above" valueNumeric="0.00"/>
<column name="is_active" valueBoolean="true"/>
<column name="created_by" valueNumeric="1"/>
<column name="created_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
<column name="last_modified_by" valueNumeric="1"/>
<column name="last_modified_on_utc" valueComputed="CURRENT_TIMESTAMP"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Per-customer override of (per_txn_max, per_day_max) for a given channel.
NULL on either column inherits the global default from m_transfer_limit_config.
Effective dating lets us raise/lower a customer's limit at KYC re-review without
deleting history.
-->
<changeSet id="0303_create_m_customer_transfer_limit" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_customer_transfer_limit"/></not></preConditions>
<createTable tableName="m_customer_transfer_limit">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="channel" type="VARCHAR(16)">
<constraints nullable="false"/>
</column>
<column name="per_txn_max" type="DECIMAL(19,4)"/>
<column name="per_day_max" type="DECIMAL(19,4)"/>
<column name="effective_from" type="DATE">
<constraints nullable="false"/>
</column>
<column name="effective_to" type="DATE"/>
<column name="created_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="version" type="BIGINT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="m_customer_transfer_limit" baseColumnNames="client_id"
constraintName="fk_cust_transfer_limit_client"
referencedTableName="m_client" referencedColumnNames="id"/>
<addUniqueConstraint tableName="m_customer_transfer_limit"
columnNames="client_id, channel, effective_from"
constraintName="uq_cust_transfer_limit_scope"/>
<createIndex tableName="m_customer_transfer_limit" indexName="idx_cust_transfer_limit_lookup">
<column name="client_id"/>
<column name="channel"/>
<column name="effective_from"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
One row per customer-initiated outward fund transfer (INTERNAL/IMPS/NEFT/RTGS).
Distinct from m_imps_transaction which tracks INBOUND IMPS calls Fineract
receives as the bank under §6.2.x.
Lifecycle:
INITIATED → VALIDATED → VERIFIED → IN_PROGRESS → SUCCESS / DEEMED / FAILED / REVERSED
Plus an approval axis: AUTO / PENDING_APPROVAL / APPROVED / REJECTED
-->
<changeSet id="0304_create_m_fund_transfer" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_fund_transfer"/></not></preConditions>
<createTable tableName="m_fund_transfer">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="txn_ref" type="VARCHAR(64)">
<constraints nullable="false" unique="true" uniqueConstraintName="uq_fund_transfer_ref"/>
</column>
<column name="channel" type="VARCHAR(16)">
<constraints nullable="false"/>
</column>
<column name="direction" type="VARCHAR(8)">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(24)">
<constraints nullable="false"/>
</column>
<column name="approval_status" type="VARCHAR(16)" defaultValue="AUTO">
<constraints nullable="false"/>
</column>
<column name="response_code" type="VARCHAR(8)"/>
<column name="response_message" type="VARCHAR(255)"/>
<!-- Cashier (initiator) + customer (owner of payer account) -->
<column name="cashier_user_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<!-- Payer side -->
<column name="payer_account_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="payer_account_number" type="VARCHAR(64)">
<constraints nullable="false"/>
</column>
<!-- Payee side -->
<column name="payee_account_number" type="VARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="payee_ifsc" type="VARCHAR(11)"/>
<column name="payee_name" type="VARCHAR(255)"/>
<column name="payee_mask_name" type="VARCHAR(255)"/>
<column name="amount" type="DECIMAL(19,4)">
<constraints nullable="false"/>
</column>
<column name="remarks" type="VARCHAR(255)"/>
<!-- External handles -->
<column name="internal_txn_id" type="BIGINT"/>
<column name="switch_txn_id" type="VARCHAR(64)"/>
<column name="utr" type="VARCHAR(64)"/>
<!-- Maker-checker linkage -->
<column name="command_source_id" type="BIGINT"/>
<column name="initiated_at" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="settled_at" type="DATETIME"/>
<column name="created_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="version" type="BIGINT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="m_fund_transfer" baseColumnNames="cashier_user_id"
constraintName="fk_fund_transfer_cashier"
referencedTableName="m_appuser" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_fund_transfer" baseColumnNames="client_id"
constraintName="fk_fund_transfer_client"
referencedTableName="m_client" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_fund_transfer" baseColumnNames="payer_account_id"
constraintName="fk_fund_transfer_payer_acc"
referencedTableName="m_savings_account" referencedColumnNames="id"/>
<!-- Cashier's pipeline + daily-limit summation -->
<createIndex tableName="m_fund_transfer" indexName="idx_fund_transfer_payer_initiated">
<column name="payer_account_id"/>
<column name="initiated_at"/>
</createIndex>
<!-- Operations queues -->
<createIndex tableName="m_fund_transfer" indexName="idx_fund_transfer_channel_status">
<column name="channel"/>
<column name="status"/>
</createIndex>
<!-- Reconciliation by switch handles -->
<createIndex tableName="m_fund_transfer" indexName="idx_fund_transfer_utr">
<column name="utr"/>
</createIndex>
<createIndex tableName="m_fund_transfer" indexName="idx_fund_transfer_switch_txn">
<column name="switch_txn_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Append-only audit log of every state change on a m_fund_transfer row. Mandatory
for fraud investigations and RBI reconciliation queries.
-->
<changeSet id="0305_create_m_fund_transfer_event" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_fund_transfer_event"/></not></preConditions>
<createTable tableName="m_fund_transfer_event">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="fund_transfer_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="from_status" type="VARCHAR(24)"/>
<column name="to_status" type="VARCHAR(24)">
<constraints nullable="false"/>
</column>
<column name="event" type="VARCHAR(48)">
<constraints nullable="false"/>
</column>
<column name="payload_json" type="CLOB"/>
<column name="created_at" type="DATETIME" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_fund_transfer_event" baseColumnNames="fund_transfer_id"
constraintName="fk_fund_transfer_event_parent"
referencedTableName="m_fund_transfer" referencedColumnNames="id"/>
<createIndex tableName="m_fund_transfer_event" indexName="idx_fund_transfer_event_parent_created">
<column name="fund_transfer_id"/>
<column name="created_at"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Short-lived token issued at /v1/transfers/verify (BANL step) and redeemed at
/v1/transfers (actual debit). Carries the locked transfer params so the
customer can't change account number / amount between confirming the masked
name and the debit landing. Also doubles as the idempotency key — a token
already in REDEEMED state returns the cached transfer ref.
-->
<changeSet id="0306_create_m_verification_token" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_verification_token"/></not></preConditions>
<createTable tableName="m_verification_token">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="token" type="VARCHAR(64)">
<constraints nullable="false" unique="true" uniqueConstraintName="uq_verification_token"/>
</column>
<!-- Locked params (cashier cannot vary these between verify and debit) -->
<column name="channel" type="VARCHAR(16)">
<constraints nullable="false"/>
</column>
<column name="direction" type="VARCHAR(8)">
<constraints nullable="false"/>
</column>
<column name="cashier_user_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="payer_account_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="payee_account_number" type="VARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="payee_ifsc" type="VARCHAR(11)"/>
<column name="payee_mask_name" type="VARCHAR(255)"/>
<column name="amount" type="DECIMAL(19,4)">
<constraints nullable="false"/>
</column>
<!-- Lifecycle -->
<column name="status" type="VARCHAR(16)" defaultValue="ISSUED">
<constraints nullable="false"/>
</column>
<column name="issued_at" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="expires_at" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="redeemed_at" type="DATETIME"/>
<column name="redeemed_fund_transfer_id" type="BIGINT"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_verification_token" baseColumnNames="cashier_user_id"
constraintName="fk_verification_token_cashier"
referencedTableName="m_appuser" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_verification_token" baseColumnNames="client_id"
constraintName="fk_verification_token_client"
referencedTableName="m_client" referencedColumnNames="id"/>
<createIndex tableName="m_verification_token" indexName="idx_verification_token_expires">
<column name="status"/>
<column name="expires_at"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Permissions for fund transfer (FUNDTRANSFER entity) and its sibling admin
entities (BANKIFSC, CUSTOMERTRANSFERLIMIT). can_maker_checker=true on the
sensitive ones drives Fineract's command-pattern approval queue.
-->
<changeSet id="0307_seed_fund_transfer_permissions" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_FUNDTRANSFER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="transaction_savings"/>
<column name="code" value="CREATE_FUNDTRANSFER"/>
<column name="entity_name" value="FUNDTRANSFER"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="transaction_savings"/>
<column name="code" value="APPROVE_FUNDTRANSFER"/>
<column name="entity_name" value="FUNDTRANSFER"/>
<column name="action_name" value="APPROVE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="transaction_savings"/>
<column name="code" value="REJECT_FUNDTRANSFER"/>
<column name="entity_name" value="FUNDTRANSFER"/>
<column name="action_name" value="REJECT"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="transaction_savings"/>
<column name="code" value="READ_FUNDTRANSFER"/>
<column name="entity_name" value="FUNDTRANSFER"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="CREATE_BANKIFSC"/>
<column name="entity_name" value="BANKIFSC"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="UPDATE_BANKIFSC"/>
<column name="entity_name" value="BANKIFSC"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="READ_BANKIFSC"/>
<column name="entity_name" value="BANKIFSC"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="CREATE_CUSTOMERTRANSFERLIMIT"/>
<column name="entity_name" value="CUSTOMERTRANSFERLIMIT"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="UPDATE_CUSTOMERTRANSFERLIMIT"/>
<column name="entity_name" value="CUSTOMERTRANSFERLIMIT"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="READ_CUSTOMERTRANSFERLIMIT"/>
<column name="entity_name" value="CUSTOMERTRANSFERLIMIT"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Bootstrap permission grants for the fund-transfer module.
Two role buckets:
- 'Super user' gets EVERYTHING (initiate + read + approve + reject + admin tables).
- Every role attached to 'bankadmin' gets the cashier subset (initiate + read).
Matches the pattern used in 0291. Bank-side roles (Bank Cashier, Branch Manager)
can be created in Manage Roles UI later; this migration unblocks the immediate
"log in as bankadmin and use the new endpoints" path.
Idempotent via NOT EXISTS. No-op on installs missing the target users/roles.
-->
<changeSet id="0308_grant_fund_transfer_to_superuser" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Super user'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_FUNDTRANSFER'</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r
CROSS JOIN m_permission p
WHERE r.name = 'Super user'
AND p.code IN (
'CREATE_FUNDTRANSFER', 'READ_FUNDTRANSFER',
'APPROVE_FUNDTRANSFER', 'REJECT_FUNDTRANSFER',
'CREATE_BANKIFSC', 'UPDATE_BANKIFSC', 'READ_BANKIFSC',
'CREATE_CUSTOMERTRANSFERLIMIT', 'UPDATE_CUSTOMERTRANSFERLIMIT', 'READ_CUSTOMERTRANSFERLIMIT'
)
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
</sql>
</changeSet>
<changeSet id="0308_grant_fund_transfer_to_bankadmin_roles" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_appuser WHERE username = 'bankadmin'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_FUNDTRANSFER'</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT ur.role_id, p.id
FROM m_appuser u
JOIN m_appuser_role ur ON ur.appuser_id = u.id
CROSS JOIN m_permission p
WHERE u.username = 'bankadmin'
AND p.code IN ('CREATE_FUNDTRANSFER', 'READ_FUNDTRANSFER', 'READ_BANKIFSC', 'READ_CUSTOMERTRANSFERLIMIT')
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- Phase 3 wiring: two Quartz jobs + RECALL_FUNDTRANSFER permission. -->
<changeSet author="fineract" id="0309_imps_deemed_reconcile_job">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'IMPS DEEMED Reconciliation'</sqlCheck>
</preConditions>
<!--
Polls the IMPS switch (ReqChkTxn) for every DEEMED transfer in the [24h ago, 30s ago]
window. Cron: every 30 seconds. Adjust via Manage Jobs in the UI if too aggressive.
-->
<insert tableName="job">
<column name="name" value="IMPS DEEMED Reconciliation"/>
<column name="short_name" value="IMPS_REC"/>
<column name="display_name" value="IMPS DEEMED Reconciliation"/>
<column name="cron_expression" value="0/30 * * * * ?"/>
<column name="create_time" valueDate="${current_datetime}"/>
<column name="task_priority" valueNumeric="5"/>
<column name="group_name"/>
<column name="previous_run_start_time"/>
<column name="job_key" value="IMPS DEEMED ReconciliationDetail1 _ DEFAULT"/>
<column name="initializing_errorlog"/>
<column name="is_active" valueBoolean="false"/>
<column name="currently_running" valueBoolean="false"/>
<column name="updates_allowed" valueBoolean="true"/>
<column name="scheduler_group" valueNumeric="0"/>
<column name="is_misfired" valueBoolean="false"/>
<column name="node_id" valueNumeric="1"/>
<column name="is_mismatched_job" valueBoolean="true"/>
</insert>
</changeSet>
<changeSet author="fineract" id="0309_neft_batch_process_job">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'NEFT Batch Process'</sqlCheck>
</preConditions>
<!--
Processes QUEUED NEFT rows through the switch. Cron: top and bottom of every hour,
matching the NEFT half-hourly settlement schedule.
-->
<insert tableName="job">
<column name="name" value="NEFT Batch Process"/>
<column name="short_name" value="NEFT_BAT"/>
<column name="display_name" value="NEFT Batch Process"/>
<column name="cron_expression" value="0 0,30 * * * ?"/>
<column name="create_time" valueDate="${current_datetime}"/>
<column name="task_priority" valueNumeric="5"/>
<column name="group_name"/>
<column name="previous_run_start_time"/>
<column name="job_key" value="NEFT Batch ProcessDetail1 _ DEFAULT"/>
<column name="initializing_errorlog"/>
<column name="is_active" valueBoolean="false"/>
<column name="currently_running" valueBoolean="false"/>
<column name="updates_allowed" valueBoolean="true"/>
<column name="scheduler_group" valueNumeric="0"/>
<column name="is_misfired" valueBoolean="false"/>
<column name="node_id" valueNumeric="1"/>
<column name="is_mismatched_job" valueBoolean="true"/>
</insert>
</changeSet>
<changeSet id="0309_seed_recall_fund_transfer_permission" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'RECALL_FUNDTRANSFER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="transaction_savings"/>
<column name="code" value="RECALL_FUNDTRANSFER"/>
<column name="entity_name" value="FUNDTRANSFER"/>
<column name="action_name" value="RECALL"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="0309_grant_recall_to_superuser" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Super user'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'RECALL_FUNDTRANSFER'</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r
CROSS JOIN m_permission p
WHERE r.name = 'Super user'
AND p.code = 'RECALL_FUNDTRANSFER'
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Creates two purpose-built roles for the fund-transfer module so admins don't have to
wire permissions by hand in Manage Roles UI. Existing 0308 grants to Super user +
bankadmin stay; these add the canonical pair.
Bank Cashier: initiates transfers, reads bank IFSC list, reads customer limit view
Branch Manager: everything the cashier can do, plus approve / reject / recall, plus
create / update / read per-customer limit overrides
-->
<changeSet id="0310_create_bank_cashier_role" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_role WHERE name = 'Bank Cashier'</sqlCheck>
</preConditions>
<insert tableName="m_role">
<column name="name" value="Bank Cashier"/>
<column name="description" value="Teller-side fund-transfer role. Initiate IMPS / NEFT / RTGS / INTERNAL transfers via /v1/transfers. No approval / recall rights."/>
<column name="is_disabled" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="0310_grant_bank_cashier_permissions" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Bank Cashier'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_FUNDTRANSFER'</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r
CROSS JOIN m_permission p
WHERE r.name = 'Bank Cashier'
AND p.code IN ('CREATE_FUNDTRANSFER', 'READ_FUNDTRANSFER', 'READ_BANKIFSC', 'READ_CUSTOMERTRANSFERLIMIT')
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
</sql>
</changeSet>
<changeSet id="0310_create_branch_manager_role" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_role WHERE name = 'Branch Manager'</sqlCheck>
</preConditions>
<insert tableName="m_role">
<column name="name" value="Branch Manager"/>
<column name="description" value="Approves / rejects / recalls fund transfers above maker-checker threshold. Can also create and update per-customer limit overrides. Includes all Bank Cashier rights."/>
<column name="is_disabled" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="0310_grant_branch_manager_permissions" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_role WHERE name = 'Branch Manager'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_FUNDTRANSFER'</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r
CROSS JOIN m_permission p
WHERE r.name = 'Branch Manager'
AND p.code IN (
'CREATE_FUNDTRANSFER', 'READ_FUNDTRANSFER',
'APPROVE_FUNDTRANSFER', 'REJECT_FUNDTRANSFER', 'RECALL_FUNDTRANSFER',
'READ_BANKIFSC',
'CREATE_CUSTOMERTRANSFERLIMIT', 'UPDATE_CUSTOMERTRANSFERLIMIT', 'READ_CUSTOMERTRANSFERLIMIT'
)
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
)
</sql>
</changeSet>
<!--
Register the verification-token cleanup Quartz job. Inactive by default; ops should
flip is_active to true once they're happy with the retention window
(fineract.transfer.verification-token.retention-hours, default 24h).
Cron: every 15 minutes — light query, no concurrency concerns.
-->
<changeSet author="fineract" id="0310_verification_token_cleanup_job">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'Verification Token Cleanup'</sqlCheck>
</preConditions>
<insert tableName="job">
<column name="name" value="Verification Token Cleanup"/>
<column name="short_name" value="VTOK_CLN"/>
<column name="display_name" value="Verification Token Cleanup"/>
<column name="cron_expression" value="0 0/15 * * * ?"/>
<column name="create_time" valueDate="${current_datetime}"/>
<column name="task_priority" valueNumeric="5"/>
<column name="group_name"/>
<column name="previous_run_start_time"/>
<column name="job_key" value="Verification Token CleanupDetail1 _ DEFAULT"/>
<column name="initializing_errorlog"/>
<column name="is_active" valueBoolean="false"/>
<column name="currently_running" valueBoolean="false"/>
<column name="updates_allowed" valueBoolean="true"/>
<column name="scheduler_group" valueNumeric="0"/>
<column name="is_misfired" valueBoolean="false"/>
<column name="node_id" valueNumeric="1"/>
<column name="is_mismatched_job" valueBoolean="true"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!--
Bootstraps the suspense GL account that OFF_US transfers park money in while in flight.
Topology:
House client (m_client, external_id='FUNDTRANSFER_SUSPENSE_HOUSE')
└── Suspense savings account (m_savings_account, external_id='FUNDTRANSFER_SUSPENSE')
The savings account inherits its currency / interest / overdraft fields from the first
active savings product so we don't have to hardcode env-specific defaults. If no savings
product exists yet, both changesets MARK_RAN — admin must create a product first then
let ops re-run the changelog (or do the inserts by hand).
SuspenseGlService resolves these at runtime via external_id, so the auto-generated id
doesn't need to be threaded through application.properties.
-->
<changeSet id="0311_create_house_client" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_client WHERE external_id = 'FUNDTRANSFER_SUSPENSE_HOUSE'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_office WHERE id = 1</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_client (
account_no, external_id, status_enum, office_id, display_name,
fullname, is_staff, activation_date, office_joining_date,
submittedon_date, legal_form_enum
)
VALUES (
'HOUSE0000001', 'FUNDTRANSFER_SUSPENSE_HOUSE', 300, 1, 'House (Fund Transfer Suspense)',
'House (Fund Transfer Suspense)', false, CURRENT_DATE, CURRENT_DATE,
CURRENT_DATE, 2
)
</sql>
</changeSet>
<changeSet id="0311_create_suspense_savings_account" author="fineract">
<preConditions onFail="MARK_RAN" onFailMessage="At least one savings product required to bootstrap the fund-transfer suspense account">
<and>
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_savings_account WHERE external_id = 'FUNDTRANSFER_SUSPENSE'</sqlCheck>
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM m_client WHERE external_id = 'FUNDTRANSFER_SUSPENSE_HOUSE'</sqlCheck>
<sqlCheck expectedResult="1">
SELECT CASE WHEN COUNT(*) >= 1 THEN 1 ELSE 0 END FROM m_savings_product
</sqlCheck>
</and>
</preConditions>
<sql>
INSERT INTO m_savings_account (
account_no, external_id, client_id, product_id,
status_enum, sub_status_enum, account_type_enum, deposit_type_enum,
submittedon_date, activatedon_date,
currency_code, currency_digits, currency_multiplesof,
nominal_annual_interest_rate,
interest_compounding_period_enum, interest_posting_period_enum,
interest_calculation_type_enum, interest_calculation_days_in_year_type_enum,
withdrawal_fee_for_transfer, allow_overdraft, overdraft_limit,
nominal_annual_interest_rate_overdraft, min_overdraft_for_interest_calculation,
enforce_min_required_balance, account_balance_derived,
version, withhold_tax
)
SELECT
'SUSPENSE01', 'FUNDTRANSFER_SUSPENSE',
(SELECT id FROM m_client WHERE external_id = 'FUNDTRANSFER_SUSPENSE_HOUSE'),
sp.id,
300, 0, 1, 100,
CURRENT_DATE, CURRENT_DATE,
sp.currency_code, sp.currency_digits, sp.currency_multiplesof,
sp.nominal_annual_interest_rate,
sp.interest_compounding_period_enum, sp.interest_posting_period_enum,
sp.interest_calculation_type_enum, sp.interest_calculation_days_in_year_type_enum,
false, false, NULL,
0.000000, 0.000000,
false, 0.000000,
1, false
FROM m_savings_product sp
WHERE sp.id = (SELECT MIN(id) FROM m_savings_product)
</sql>
</changeSet>
</databaseChangeLog>