feat(clearing): add CTS/NACH clearing modules with real NPCI integration
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

- Inward/outward CTS clearing (capture, validate, settle, dishonour), positive pay,
  settlement/reconciliation, and NACH clearing modules
- Real, separate NPCI clearing-house integration: hybrid RSA+AES envelope crypto,
  bank self-registration, outward batch submission, and an inward push webhook
- Outward clearing is now session-batched: a 2-hourly scheduled job (plus the
  existing manual "Send Pending Batch" fallback) bundles CAPTURED cheques instead
  of sending them one at a time
- Batch-summary read APIs and date/text filters across inward batches, outward
  presentments, and outward batches

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-21 15:46:52 +05:30
co-authored by Claude Sonnet 5
parent 383dfdc6e9
commit b433edfdfe
162 changed files with 12826 additions and 63186 deletions
@@ -690,3 +690,22 @@ fineract.email.support-address=${FINERACT_EMAIL_SUPPORT_ADDRESS:support@cbsnexor
# Frontend base URL — used for email links (voting, etc.)
cbs.frontend.base-url=${CBS_FRONTEND_BASE_URL:https://cbs.godemo.in}
# ── NPCI Clearing House Integration ──────────────────────────────────────────
# Real, separate NPCI service (see /npci-clearing-house) — this CBS registers itself as a bank there
# (its own key pair, generated locally, only the public half is ever sent) and submits outward CTS
# batches to it. NPCI's own public key is fetched live via GET {base-url}/npci/public-key, never configured
# here — no key material is ever copy-pasted between the two services.
fineract.npci.base-url=${FINERACT_NPCI_BASE_URL:http://localhost:8444}
fineract.npci.bank-code=${FINERACT_NPCI_BANK_CODE:CBSHOME01}
fineract.npci.bank-name=${FINERACT_NPCI_BANK_NAME:CBSnexora Home Bank}
fineract.npci.ifsc-prefix=${FINERACT_NPCI_IFSC_PREFIX:CBSH}
# Where NPCI can push routed packages destined for this bank: POST /api/v1/npci/inward-push. NOTE this
# Fineract fork identifies the tenant via the Fineract-Platform-TenantId HEADER, not a URL path segment —
# there is no "/default/" (or any tenant) segment in the path itself; NPCI's push client must send that
# header (defaults to "default", the only tenant this dev setup has).
# ALSO NOTE: CBS's own HTTPS certificate is self-signed in this dev setup — NPCI's JDK HttpClient will fail
# TLS validation against it unless NPCI is separately configured to trust it. Until then, pushes here
# will fail and NPCI gracefully falls back to the poll model (GET /clearing/inward/{bankCode}).
fineract.npci.inward-callback-url=${FINERACT_NPCI_INWARD_CALLBACK_URL:https://localhost:8443/fineract-provider/api/v1/npci/inward-push}
fineract.npci.timeout-ms=${FINERACT_NPCI_TIMEOUT_MS:10000}
@@ -410,4 +410,27 @@
<include file="parts/0387_chequeleaf_serial_unique_per_lot.xml" relativeToChangelogFile="true"/>
<include file="parts/0388_chequeleaf_returned_status.xml" relativeToChangelogFile="true"/>
<include file="parts/0389_chequebook_pending_request_hold.xml" relativeToChangelogFile="true"/>
<include file="parts/0390_clearing_shared_reference.xml" relativeToChangelogFile="true"/>
<include file="parts/0391_cts_inward_clearing.xml" relativeToChangelogFile="true"/>
<include file="parts/0392_cts_outward_clearing.xml" relativeToChangelogFile="true"/>
<include file="parts/0393_clearing_settlement_reconciliation.xml" relativeToChangelogFile="true"/>
<include file="parts/0394_nach_clearing.xml" relativeToChangelogFile="true"/>
<include file="parts/0395_clearing_permissions.xml" relativeToChangelogFile="true"/>
<include file="parts/0396_cts_cheque_image_iqa.xml" relativeToChangelogFile="true"/>
<include file="parts/0397_positive_pay_system.xml" relativeToChangelogFile="true"/>
<include file="parts/0398_outward_npci_envelope.xml" relativeToChangelogFile="true"/>
<include file="parts/0399_outward_cts_file.xml" relativeToChangelogFile="true"/>
<include file="parts/0400_outward_iqa_stoppayment.xml" relativeToChangelogFile="true"/>
<include file="parts/0401_outward_cts_missing_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0402_outward_send_batch_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0403_outward_clearing_batch_job.xml" relativeToChangelogFile="true"/>
<include file="parts/0404_cts_cheque_image_store.xml" relativeToChangelogFile="true"/>
<include file="parts/0405_cts_npci_inward_package.xml" relativeToChangelogFile="true"/>
<include file="parts/0406_clearing_crypto_audit.xml" relativeToChangelogFile="true"/>
<include file="parts/0407_remove_clearing_crypto_simulation.xml" relativeToChangelogFile="true"/>
<include file="parts/0408_clearing_own_npci_identity.xml" relativeToChangelogFile="true"/>
<include file="parts/0409_outward_npci_response.xml" relativeToChangelogFile="true"/>
<include file="parts/0410_npci_inward_push_log.xml" relativeToChangelogFile="true"/>
<include file="parts/0411_inward_item_source_details.xml" relativeToChangelogFile="true"/>
<include file="parts/0412_activate_outward_clearing_batch_job.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Shared reference data for the CTS + NACH clearing modules:
- m_clearing_bank : the (simulated) clearing network's banks and their RSA key pairs
- m_clearing_return_reason_code : standardized return/dishonour codes (spec section 14), shared CTS+NACH
- m_clearing_gl_config : maps clearing suspense/settlement roles to GL accounts (admin-configured)
- m_clearing_notification : simulated SMS/email log for pass/return/settlement events
Bank rows are NOT seeded here because each needs a real RSA key pair generated at runtime by
ClearingCryptoService; they are created via POST /v1/clearing/banks (or the one-shot bootstrap action).
Reason codes ARE seeded (idempotently) so decisioning dropdowns work out of the box.
Every changeSet is guarded so this migration is a safe no-op where already applied.
-->
<changeSet id="0390-clearing-bank" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_clearing_bank"/></not>
</preConditions>
<createTable tableName="m_clearing_bank">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="bank_name" type="VARCHAR(150)">
<constraints nullable="false"/>
</column>
<column name="ifsc_prefix" type="VARCHAR(20)">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_clearing_bank_ifsc_prefix"/>
</column>
<column name="role" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="public_key" type="TEXT">
<constraints nullable="false"/>
</column>
<column name="private_key" type="TEXT"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet id="0390-clearing-return-reason-code" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_clearing_return_reason_code"/></not>
</preConditions>
<createTable tableName="m_clearing_return_reason_code">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="code" type="VARCHAR(10)">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_clearing_return_reason_code"/>
</column>
<column name="description" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="category" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="applies_to" type="VARCHAR(10)" defaultValue="BOTH">
<constraints nullable="false"/>
</column>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet id="0390-clearing-return-reason-code-seed" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '01', 'Insufficient Funds', 'FINANCIAL', 'BOTH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '01');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '02', 'Payment Stopped by Drawer', 'INSTRUMENT', 'CTS', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '02');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '03', 'Account Closed', 'ACCOUNT', 'BOTH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '03');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '04', 'Signature Differs / Mismatch', 'SIGNATURE', 'CTS', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '04');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '05', 'Alteration Requires Drawer''s Authentication', 'INSTRUMENT', 'CTS', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '05');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '06', 'Amount in Words and Figures Differ', 'INSTRUMENT', 'CTS', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '06');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '07', 'Post Dated / Stale Cheque', 'INSTRUMENT', 'CTS', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '07');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '08', 'Refer to Drawer', 'ACCOUNT', 'CTS', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '08');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '09', 'No Such Account', 'ACCOUNT', 'BOTH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '09');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '10', 'Account Frozen / Dormant', 'ACCOUNT', 'BOTH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '10');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '11', 'Image Quality (IQA) Failure', 'TECHNICAL', 'CTS', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '11');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '12', 'Duplicate Presentment', 'TECHNICAL', 'BOTH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '12');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '20', 'Mandate Not Registered', 'MANDATE', 'NACH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '20');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '21', 'Mandate Expired or Cancelled', 'MANDATE', 'NACH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '21');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '22', 'Amount Exceeds Mandate Limit', 'MANDATE', 'NACH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '22');
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '99', 'Other (see remarks)', 'OTHER', 'BOTH', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '99');
</sql>
</changeSet>
<changeSet id="0390-clearing-gl-config" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_clearing_gl_config"/></not>
</preConditions>
<createTable tableName="m_clearing_gl_config">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="config_key" type="VARCHAR(50)">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_clearing_gl_config_key"/>
</column>
<column name="description" type="VARCHAR(200)"/>
<column name="gl_account_id" type="BIGINT">
<constraints foreignKeyName="fk_clearing_gl_config_gl" references="acc_gl_account(id)"/>
</column>
</createTable>
</changeSet>
<changeSet id="0390-clearing-gl-config-seed" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_clearing_gl_config (config_key, description, gl_account_id)
SELECT 'CTS_INWARD_SUSPENSE', 'CTS inward clearing suspense (paying side)', NULL
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_gl_config WHERE config_key = 'CTS_INWARD_SUSPENSE');
INSERT INTO m_clearing_gl_config (config_key, description, gl_account_id)
SELECT 'CTS_OUTWARD_SUSPENSE', 'CTS outward clearing suspense (collecting side)', NULL
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_gl_config WHERE config_key = 'CTS_OUTWARD_SUSPENSE');
INSERT INTO m_clearing_gl_config (config_key, description, gl_account_id)
SELECT 'NACH_SUSPENSE', 'NACH clearing suspense', NULL
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_gl_config WHERE config_key = 'NACH_SUSPENSE');
</sql>
</changeSet>
<changeSet id="0390-clearing-notification" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_clearing_notification"/></not>
</preConditions>
<createTable tableName="m_clearing_notification">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="channel" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="recipient" type="VARCHAR(150)"/>
<column name="event" type="VARCHAR(30)">
<constraints nullable="false"/>
</column>
<column name="clearing_type" type="VARCHAR(10)"/>
<column name="ref_type" type="VARCHAR(40)"/>
<column name="ref_id" type="BIGINT"/>
<column name="message" type="VARCHAR(1000)">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(15)" defaultValue="SIMULATED">
<constraints nullable="false"/>
</column>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
CTS inward clearing: a batch (session) of cheques drawn on THIS bank, received (simulated) from the
clearing house, and the individual items within it. Each item carries its captured MICR data, the
result of every validation rule, and the pass/return decision. An item links to an existing
m_cheque_leaf where one matches (by cheque number + account), so a pass/return can drive that leaf's
USED/RETURNED status. Safe no-op where already applied.
-->
<changeSet id="0391-inward-clearing-batch" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_inward_clearing_batch"/></not>
</preConditions>
<createTable tableName="m_inward_clearing_batch">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="batch_reference" type="VARCHAR(100)">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_inward_clearing_batch_ref"/>
</column>
<column name="session_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="presentment_type" type="VARCHAR(10)" defaultValue="MICR">
<constraints nullable="false"/>
</column>
<column name="source_file" type="VARCHAR(200)"/>
<column name="inward_envelope" type="TEXT"/>
<column name="total_item_count" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(15)" defaultValue="RECEIVED">
<constraints nullable="false"/>
</column>
<column name="office_id" type="BIGINT"/>
<column name="received_by" type="BIGINT"/>
<column name="received_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="validated_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="decisioned_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="settled_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
<changeSet id="0391-inward-clearing-item" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_inward_clearing_item"/></not>
</preConditions>
<createTable tableName="m_inward_clearing_item">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="batch_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_inward_item_batch" references="m_inward_clearing_batch(id)"/>
</column>
<column name="cheque_leaf_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_inward_item_leaf" references="m_cheque_leaf(id)"/>
</column>
<column name="account_type" type="VARCHAR(10)"/>
<column name="account_id" type="BIGINT"/>
<column name="cheque_number" type="VARCHAR(25)">
<constraints nullable="false"/>
</column>
<column name="micr_code" type="VARCHAR(20)"/>
<column name="ifsc" type="VARCHAR(15)"/>
<column name="payee_name" type="VARCHAR(150)"/>
<column name="amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="amount_in_words" type="VARCHAR(300)"/>
<column name="instrument_date" type="DATE"/>
<column name="presented_date" type="DATE"/>
<!-- validation results: nullable Boolean = not-yet-checked; signature_ok null = not manually reviewed -->
<column name="account_check_passed" type="BOOLEAN"/>
<column name="balance_check_passed" type="BOOLEAN"/>
<column name="signature_ok" type="BOOLEAN"/>
<column name="duplicate_check_passed" type="BOOLEAN"/>
<column name="stop_payment_check_passed" type="BOOLEAN"/>
<column name="amount_words_figures_match" type="BOOLEAN"/>
<column name="stale_or_postdated" type="BOOLEAN"/>
<column name="validation_detail" type="VARCHAR(1000)"/>
<!-- decision -->
<column name="decision" type="VARCHAR(10)" defaultValue="PENDING">
<constraints nullable="false"/>
</column>
<column name="return_reason_code" type="VARCHAR(10)"/>
<column name="decision_reason" type="VARCHAR(500)"/>
<column name="decisioned_by" type="BIGINT"/>
<column name="decisioned_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="item_status" type="VARCHAR(15)" defaultValue="RECEIVED">
<constraints nullable="false"/>
</column>
<column name="hold_placed" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="hold_amount" type="DECIMAL(19,6)"/>
<column name="settlement_batch_id" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
CTS outward clearing: cheques our own customers deposited that are drawn on OTHER banks, captured for
onward presentation to the clearing house. A flat entity (no batch/item split needed per spec) with a
manually-advanced status. Safe no-op where already applied.
-->
<changeSet id="0392-outward-clearing-presentment" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_outward_clearing_presentment"/></not>
</preConditions>
<createTable tableName="m_outward_clearing_presentment">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="depositing_account_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="depositing_account_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="drawer_bank_micr" type="VARCHAR(20)"/>
<column name="drawer_ifsc" type="VARCHAR(15)"/>
<column name="drawer_bank_name" type="VARCHAR(150)"/>
<column name="cheque_number" type="VARCHAR(25)">
<constraints nullable="false"/>
</column>
<column name="amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="payee_name" type="VARCHAR(150)"/>
<column name="deposited_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="instrument_date" type="DATE"/>
<column name="status" type="VARCHAR(25)" defaultValue="CAPTURED">
<constraints nullable="false"/>
</column>
<column name="batch_reference" type="VARCHAR(100)"/>
<column name="sent_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="dishonour_reason_code" type="VARCHAR(10)"/>
<column name="office_id" type="BIGINT"/>
<column name="settled_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Shared net-settlement and reconciliation tables for CTS + NACH (distinguished by clearing_type).
m_clearing_settlement_batch nets a decisioned session's pass/return totals and, when posted, squares
off the clearing suspense GL against the settlement (RTGS) GL. m_clearing_reconciliation records the
day-end CBS-vs-clearing-house match. Every changeSet is a safe no-op where already applied.
-->
<changeSet id="0393-clearing-settlement-batch" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_clearing_settlement_batch"/></not>
</preConditions>
<createTable tableName="m_clearing_settlement_batch">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="clearing_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="settlement_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="direction" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="source_reference" type="VARCHAR(100)"/>
<column name="source_batch_id" type="BIGINT"/>
<column name="total_passed_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="total_returned_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="net_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="passed_count" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="returned_count" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="office_id" type="BIGINT"/>
<column name="currency_code" type="VARCHAR(3)"/>
<column name="gl_transaction_id" type="VARCHAR(100)"/>
<column name="status" type="VARCHAR(15)" defaultValue="PENDING">
<constraints nullable="false"/>
</column>
<column name="prepared_by" type="BIGINT"/>
<column name="prepared_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="posted_by" type="BIGINT"/>
<column name="posted_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="rejection_reason" type="VARCHAR(500)"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet id="0393-clearing-reconciliation" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_clearing_reconciliation"/></not>
</preConditions>
<createTable tableName="m_clearing_reconciliation">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="clearing_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="reconciliation_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="source_reference" type="VARCHAR(100)"/>
<column name="cbs_total" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="clearing_house_total" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="difference" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(15)">
<constraints nullable="false"/>
</column>
<column name="detail" type="VARCHAR(1000)"/>
<column name="run_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet id="0393-clearing-settlement-gl-config-seed" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_clearing_gl_config (config_key, description, gl_account_id)
SELECT 'CLEARING_SETTLEMENT_RTGS', 'Net settlement / RTGS control account for clearing', NULL
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_gl_config WHERE config_key = 'CLEARING_SETTLEMENT_RTGS');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
NACH (National Automated Clearing House): recurring, mandate-based bulk clearing (spec sections 7-8).
- m_nach_mandate : a customer's signed authorization to debit/credit their account repeatedly
- m_nach_batch : a sponsor bank's batch of due debits/credits for a cycle
- m_nach_transaction : one debit/credit within a batch, with validation results + pass/return decision
Return details live on the transaction row (decision=RETURN + return_reason_code), mirroring the CTS
inward-item design, rather than a separate returns table. Settlement + reconciliation reuse the shared
m_clearing_settlement_batch / m_clearing_reconciliation tables (clearing_type='NACH'). Safe no-op where
already applied.
-->
<changeSet id="0394-nach-mandate" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_nach_mandate"/></not>
</preConditions>
<createTable tableName="m_nach_mandate">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="mandate_reference" type="VARCHAR(50)">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_nach_mandate_ref"/>
</column>
<column name="account_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="account_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="sponsor_bank_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_nach_mandate_sponsor" references="m_clearing_bank(id)"/>
</column>
<column name="mandate_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="amount" type="DECIMAL(19,6)"/>
<column name="max_amount" type="DECIMAL(19,6)"/>
<column name="frequency" type="VARCHAR(15)"/>
<column name="start_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="end_date" type="DATE"/>
<column name="debtor_name" type="VARCHAR(150)"/>
<column name="purpose" type="VARCHAR(150)"/>
<column name="status" type="VARCHAR(15)" defaultValue="ACTIVE">
<constraints nullable="false"/>
</column>
<column name="cancelled_reason" type="VARCHAR(500)"/>
<column name="cancelled_by" type="BIGINT"/>
<column name="cancelled_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
<changeSet id="0394-nach-batch" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_nach_batch"/></not>
</preConditions>
<createTable tableName="m_nach_batch">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="batch_reference" type="VARCHAR(100)">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_nach_batch_ref"/>
</column>
<column name="sponsor_bank_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_nach_batch_sponsor" references="m_clearing_bank(id)"/>
</column>
<column name="batch_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="value_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="total_txn_count" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(15)" defaultValue="CREATED">
<constraints nullable="false"/>
</column>
<column name="office_id" type="BIGINT"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="processed_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="settled_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
<changeSet id="0394-nach-transaction" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_nach_transaction"/></not>
</preConditions>
<createTable tableName="m_nach_transaction">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="batch_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_nach_txn_batch" references="m_nach_batch(id)"/>
</column>
<column name="mandate_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_nach_txn_mandate" references="m_nach_mandate(id)"/>
</column>
<column name="account_type" type="VARCHAR(10)"/>
<column name="account_id" type="BIGINT"/>
<column name="txn_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="amount" type="DECIMAL(19,6)" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="mandate_active_check" type="BOOLEAN"/>
<column name="amount_within_limit_check" type="BOOLEAN"/>
<column name="balance_check_passed" type="BOOLEAN"/>
<column name="account_check_passed" type="BOOLEAN"/>
<column name="duplicate_check_passed" type="BOOLEAN"/>
<column name="validation_detail" type="VARCHAR(1000)"/>
<column name="decision" type="VARCHAR(10)" defaultValue="PENDING">
<constraints nullable="false"/>
</column>
<column name="return_reason_code" type="VARCHAR(10)"/>
<column name="decision_reason" type="VARCHAR(500)"/>
<column name="decisioned_by" type="BIGINT"/>
<column name="decisioned_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="status" type="VARCHAR(15)" defaultValue="RECEIVED">
<constraints nullable="false"/>
</column>
<column name="settlement_batch_id" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 the CTS + NACH clearing modules. Every command that flows through Fineract's command
pipeline needs a matching m_permission row (grouping 'portfolio', code '<ACTION>_<ENTITY>'). Only
POST_CLEARINGSETTLEMENTBATCH is can_maker_checker=true (the one two-person-control point; a code-level
self-poster guard backs it up because ALL_FUNCTIONS bypasses the flag in this tenant). MENU_CLEARING
already exists (0231); MENU_CTSCLEARING / MENU_NACHCLEARING are added for optional finer nav gating.
Idempotent via WHERE NOT EXISTS.
-->
<changeSet id="0395-clearing-permissions" author="CBS" runOnChange="false">
<sql>
-- shared: clearing bank directory / keys
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CLEARINGBANK', 'CLEARINGBANK', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CLEARINGBANK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CLEARINGBANK', 'CLEARINGBANK', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CLEARINGBANK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'BOOTSTRAP_CLEARINGBANK', 'CLEARINGBANK', 'BOOTSTRAP', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'BOOTSTRAP_CLEARINGBANK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'ROTATEKEYS_CLEARINGBANK', 'CLEARINGBANK', 'ROTATEKEYS', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'ROTATEKEYS_CLEARINGBANK');
-- shared: return reason codes (read-only reference)
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CLEARINGRETURNREASONCODE', 'CLEARINGRETURNREASONCODE', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CLEARINGRETURNREASONCODE');
-- shared: settlement batch
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CLEARINGSETTLEMENTBATCH', 'CLEARINGSETTLEMENTBATCH', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CLEARINGSETTLEMENTBATCH');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CLEARINGSETTLEMENTBATCH', 'CLEARINGSETTLEMENTBATCH', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CLEARINGSETTLEMENTBATCH');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'POST_CLEARINGSETTLEMENTBATCH', 'CLEARINGSETTLEMENTBATCH', 'POST', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'POST_CLEARINGSETTLEMENTBATCH');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REJECT_CLEARINGSETTLEMENTBATCH', 'CLEARINGSETTLEMENTBATCH', 'REJECT', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_CLEARINGSETTLEMENTBATCH');
-- shared: reconciliation
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CLEARINGRECONCILIATION', 'CLEARINGRECONCILIATION', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CLEARINGRECONCILIATION');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CLEARINGRECONCILIATION', 'CLEARINGRECONCILIATION', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CLEARINGRECONCILIATION');
-- CTS inward batch
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_INWARDCLEARINGBATCH', 'INWARDCLEARINGBATCH', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_INWARDCLEARINGBATCH');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_INWARDCLEARINGBATCH', 'INWARDCLEARINGBATCH', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_INWARDCLEARINGBATCH');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'VALIDATE_INWARDCLEARINGBATCH', 'INWARDCLEARINGBATCH', 'VALIDATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'VALIDATE_INWARDCLEARINGBATCH');
-- CTS inward item decisioning
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_INWARDCLEARINGITEM', 'INWARDCLEARINGITEM', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_INWARDCLEARINGITEM');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'PASS_INWARDCLEARINGITEM', 'INWARDCLEARINGITEM', 'PASS', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'PASS_INWARDCLEARINGITEM');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'RETURN_INWARDCLEARINGITEM', 'INWARDCLEARINGITEM', 'RETURN', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'RETURN_INWARDCLEARINGITEM');
-- CTS outward presentment
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_OUTWARDCLEARINGPRESENTMENT', 'OUTWARDCLEARINGPRESENTMENT', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_OUTWARDCLEARINGPRESENTMENT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_OUTWARDCLEARINGPRESENTMENT', 'OUTWARDCLEARINGPRESENTMENT', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_OUTWARDCLEARINGPRESENTMENT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'SEND_OUTWARDCLEARINGPRESENTMENT', 'OUTWARDCLEARINGPRESENTMENT', 'SEND', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SEND_OUTWARDCLEARINGPRESENTMENT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'SETTLE_OUTWARDCLEARINGPRESENTMENT', 'OUTWARDCLEARINGPRESENTMENT', 'SETTLE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SETTLE_OUTWARDCLEARINGPRESENTMENT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'DISHONOUR_OUTWARDCLEARINGPRESENTMENT', 'OUTWARDCLEARINGPRESENTMENT', 'DISHONOUR', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'DISHONOUR_OUTWARDCLEARINGPRESENTMENT');
-- NACH mandate
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_NACHMANDATE', 'NACHMANDATE', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_NACHMANDATE');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_NACHMANDATE', 'NACHMANDATE', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_NACHMANDATE');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CANCEL_NACHMANDATE', 'NACHMANDATE', 'CANCEL', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CANCEL_NACHMANDATE');
-- NACH batch
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_NACHBATCH', 'NACHBATCH', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_NACHBATCH');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_NACHBATCH', 'NACHBATCH', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_NACHBATCH');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'PROCESS_NACHBATCH', 'NACHBATCH', 'PROCESS', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'PROCESS_NACHBATCH');
-- NACH transaction decisioning
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_NACHTRANSACTION', 'NACHTRANSACTION', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_NACHTRANSACTION');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'PASS_NACHTRANSACTION', 'NACHTRANSACTION', 'PASS', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'PASS_NACHTRANSACTION');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'RETURN_NACHTRANSACTION', 'NACHTRANSACTION', 'RETURN', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'RETURN_NACHTRANSACTION');
-- menus (MENU_CLEARING already seeded in 0231; add module-specific ones for optional finer gating)
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'heading_menu', 'MENU_CTSCLEARING', 'MENU', 'CTSCLEARING', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'MENU_CTSCLEARING');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'heading_menu', 'MENU_NACHCLEARING', 'MENU', 'NACHCLEARING', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'MENU_NACHCLEARING');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Cheque image capture + Image Quality Assessment (IQA) for inward clearing items (spec section 5 &
the cheque_images concept in section 11). Front/back image references travel with the presented
cheque; iqa_status (PASS / FAIL / PENDING / NA) records the automated image-quality outcome. A FAIL
surfaces in validation and lets the item be returned with reason code 11 (Image Quality Failure),
which was already seeded in 0390. Additive columns only; safe no-op where already applied.
-->
<changeSet id="0396-inward-item-image-iqa" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_inward_clearing_item"/>
<not><columnExists tableName="m_inward_clearing_item" columnName="iqa_status"/></not>
</and>
</preConditions>
<addColumn tableName="m_inward_clearing_item">
<column name="front_image_ref" type="VARCHAR(300)"/>
<column name="back_image_ref" type="VARCHAR(300)"/>
<column name="iqa_status" type="VARCHAR(10)" defaultValue="NA">
<constraints nullable="false"/>
</column>
<column name="iqa_detail" type="VARCHAR(500)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Positive Pay System (PPS) — RBI-mandated (since 2021) for cheques at or above a high-value threshold
(default INR 50,000). The drawer pre-registers the cheque's key details; inward clearing then matches a
presented high-value cheque against an ACTIVE registration and returns it (reason code 13) when there is
no registration or the details differ. Adds the registration table, a positive_pay_check result column on
the inward item, the '13' reason code, and permissions. Idempotent / safe no-op where already applied.
-->
<changeSet id="0397-positive-pay-registration" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_positive_pay_registration"/></not>
</preConditions>
<createTable tableName="m_positive_pay_registration">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="account_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="account_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="cheque_number" type="VARCHAR(25)">
<constraints nullable="false"/>
</column>
<column name="instrument_date" type="DATE"/>
<column name="amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="payee_name" type="VARCHAR(150)"/>
<column name="channel" type="VARCHAR(20)" defaultValue="BRANCH"/>
<column name="status" type="VARCHAR(15)" defaultValue="ACTIVE">
<constraints nullable="false"/>
</column>
<column name="registered_by" type="BIGINT"/>
<column name="registered_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="cancelled_reason" type="VARCHAR(500)"/>
<column name="cancelled_by" type="BIGINT"/>
<column name="cancelled_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
<createIndex tableName="m_positive_pay_registration" indexName="idx_pps_account_cheque">
<column name="account_id"/>
<column name="cheque_number"/>
</createIndex>
</changeSet>
<changeSet id="0397-inward-item-positive-pay-check" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_inward_clearing_item"/>
<not><columnExists tableName="m_inward_clearing_item" columnName="positive_pay_check"/></not>
</and>
</preConditions>
<addColumn tableName="m_inward_clearing_item">
<column name="positive_pay_check" type="BOOLEAN"/>
</addColumn>
</changeSet>
<changeSet id="0397-positive-pay-reason-code" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_clearing_return_reason_code (code, description, category, applies_to, is_active)
SELECT '13', 'Positive Pay Data Mismatch / Not Registered', 'INSTRUMENT', 'CTS', true
WHERE NOT EXISTS (SELECT 1 FROM m_clearing_return_reason_code WHERE code = '13');
</sql>
</changeSet>
<changeSet id="0397-positive-pay-permissions" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_POSITIVEPAYREGISTRATION', 'POSITIVEPAYREGISTRATION', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_POSITIVEPAYREGISTRATION');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_POSITIVEPAYREGISTRATION', 'POSITIVEPAYREGISTRATION', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_POSITIVEPAYREGISTRATION');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CANCEL_POSITIVEPAYREGISTRATION', 'POSITIVEPAYREGISTRATION', 'CANCEL', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CANCEL_POSITIVEPAYREGISTRATION');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
When an outward presentment is SENT, the collecting bank now genuinely bundles it into a file,
encrypts it for NPCI and signs it, and NPCI (simulated) decrypts/verifies and re-encrypts it for the
drawer bank. These columns store the resulting encrypted outward envelope, the destination (drawer)
bank it was routed to, and whether NPCI's signature verified — so the real crypto flow is visible on
each cheque. Additive; safe no-op where already applied.
-->
<changeSet id="0398-outward-npci-envelope" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_outward_clearing_presentment"/>
<not><columnExists tableName="m_outward_clearing_presentment" columnName="outward_envelope"/></not>
</and>
</preConditions>
<addColumn tableName="m_outward_clearing_presentment">
<column name="outward_envelope" type="TEXT"/>
<column name="routed_to_bank" type="VARCHAR(150)"/>
<column name="npci_signature_valid" type="BOOLEAN"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Stores the full, human-readable CTS outward file (batchHeader + instrument + batchFooter, in the NPCI
CTS-2010 shape) generated when a cheque is sent. The encrypted form travels to NPCI (outward_envelope);
this plaintext copy is kept for display/audit so the operator can see exactly what was sent. Additive;
safe no-op where already applied.
-->
<changeSet id="0399-outward-cts-file" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_outward_clearing_presentment"/>
<not><columnExists tableName="m_outward_clearing_presentment" columnName="cts_file_json"/></not>
</and>
</preConditions>
<addColumn tableName="m_outward_clearing_presentment">
<column name="cts_file_json" type="TEXT"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Outward presentment: stop-payment flag and cheque image/IQA fields, captured at deposit time so the
CTS outward file reflects what the operator actually entered instead of a hardcoded PASS/false.
Additive; safe no-op where already applied.
-->
<changeSet id="0400-outward-iqa-stoppayment" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_outward_clearing_presentment"/>
<not><columnExists tableName="m_outward_clearing_presentment" columnName="stop_payment_flag"/></not>
</and>
</preConditions>
<addColumn tableName="m_outward_clearing_presentment">
<column name="stop_payment_flag" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="front_image_ref" type="VARCHAR(300)"/>
<column name="back_image_ref" type="VARCHAR(300)"/>
<column name="iqa_status" type="VARCHAR(10)" defaultValue="NA">
<constraints nullable="false"/>
</column>
<column name="iqa_detail" type="VARCHAR(500)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Fields identified as missing from the outward CTS file vs. the real CTS-2010 structure: the drawer's
account number (on the drawee bank), the presenting office/branch (drives depositBranchCode), and an
optional grid zone override (real CTS files vary this by region, e.g. WZ/Western Zone Grid - Mumbai).
Additive; safe no-op where already applied.
-->
<changeSet id="0401-outward-cts-missing-fields" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_outward_clearing_presentment"/>
<not><columnExists tableName="m_outward_clearing_presentment" columnName="drawer_account_number"/></not>
</and>
</preConditions>
<addColumn tableName="m_outward_clearing_presentment">
<column name="drawer_account_number" type="VARCHAR(34)"/>
<column name="grid_zone_code" type="VARCHAR(10)"/>
<column name="grid_zone_name" type="VARCHAR(100)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Permission for the "send all pending outward cheques to NPCI as one batch" action — used by both the
manual "Send Pending Batch" button and the (disabled by default) clearing-session scheduled job.
Idempotent via WHERE NOT EXISTS.
-->
<changeSet id="0402-outward-send-batch-permission" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'SENDBATCH_OUTWARDCLEARINGPRESENTMENT', 'OUTWARDCLEARINGPRESENTMENT', 'SENDBATCH', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SENDBATCH_OUTWARDCLEARINGPRESENTMENT');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
"CTS Outward Clearing Batch Send" scheduled job — mirrors the NEFT Batch Process / Verification Token
Cleanup job seeding convention (see 0309/0310). Cron "0 0 10-18/2 * * ?" fires at 10:00, 12:00,
14:00, 16:00 and 18:00 daily (every 2 hours, 10am-6pm; cron_expression is VARCHAR(20), so the
step-range form is used instead of an explicit hour list, which would exceed that length), bundling
every CAPTURED outward presentment into one CTS file and sending it to NPCI — the exact same business
logic the manual "Send Pending Batch to NPCI" button on the Outward Clearing screen already calls
directly.
Seeded is_active=false (disabled) by request — an operator can enable it later from the standard
Admin > System > Manage Jobs screen once ready to automate outward sends; until then, only the manual
button runs it.
-->
<changeSet author="CBS" id="0403_outward_clearing_batch_job">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'CTS Outward Clearing Batch Send'</sqlCheck>
</preConditions>
<insert tableName="job">
<column name="name" value="CTS Outward Clearing Batch Send"/>
<column name="short_name" value="CTS_OUTB"/>
<column name="display_name" value="CTS Outward Clearing Batch Send"/>
<column name="cron_expression" value="0 0 10-18/2 * * ?"/>
<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="CTS Outward Clearing Batch SendDetail1 _ 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,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Real cheque image storage for inward items and outward presentments, using the same
ContentStoreService (filesystem/S3) abstraction already used by the election-candidate-symbol upload
in this fork — NOT a DB blob. Each column stores "mimeType|storagePath" (matching that same
convention); the actual bytes live wherever ContentStoreService is configured to write them. This is
separate from the existing front_image_ref/back_image_ref columns, which remain a free-text label a
user can type without uploading a real file. Additive; safe no-op where already applied.
-->
<changeSet id="0404-inward-item-image-store" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_inward_clearing_item"/>
<not><columnExists tableName="m_inward_clearing_item" columnName="front_image_store"/></not>
</and>
</preConditions>
<addColumn tableName="m_inward_clearing_item">
<column name="front_image_store" type="VARCHAR(500)"/>
<column name="back_image_store" type="VARCHAR(500)"/>
</addColumn>
</changeSet>
<changeSet id="0404-outward-presentment-image-store" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_outward_clearing_presentment"/>
<not><columnExists tableName="m_outward_clearing_presentment" columnName="front_image_store"/></not>
</and>
</preConditions>
<addColumn tableName="m_outward_clearing_presentment">
<column name="front_image_store" type="VARCHAR(500)"/>
<column name="back_image_store" type="VARCHAR(500)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Closes the "what does NPCI actually forward to the destination/drawee bank" gap: until now the outward
send flow stopped at "NPCI decrypted + verified our batch" and never modelled NPCI's own re-split/re-sign/
re-encrypt step towards each destination bank, and the inward receive flow accepted an opaque
client-supplied "inwardEnvelope" string instead of genuinely building/signing/encrypting one. Both sides
now produce a real inwardPackageHeader/instruments/inwardPackageFooter JSON, NPCI-signed and encrypted
for the destination bank's key, mirroring the existing outward batchHeader/instruments/batchFooter file.
-->
<changeSet id="0405-outward-npci-inward-package" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_outward_clearing_presentment"/>
<not><columnExists tableName="m_outward_clearing_presentment" columnName="npci_inward_package_json"/></not>
</and>
</preConditions>
<addColumn tableName="m_outward_clearing_presentment">
<column name="npci_inward_package_json" type="TEXT"/>
<column name="npci_inward_envelope" type="TEXT"/>
</addColumn>
</changeSet>
<changeSet id="0405-inward-batch-npci-package" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_inward_clearing_batch"/>
<not><columnExists tableName="m_inward_clearing_batch" columnName="npci_processing_reference"/></not>
</and>
</preConditions>
<addColumn tableName="m_inward_clearing_batch">
<column name="presenting_bank_code" type="VARCHAR(15)"/>
<column name="presenting_bank_name" type="VARCHAR(150)"/>
<column name="npci_processing_reference" type="VARCHAR(50)"/>
<column name="inward_package_json" type="TEXT"/>
<column name="npci_signature_valid" type="BOOLEAN"/>
<column name="return_tat_deadline" type="DATE"/>
<column name="acknowledgement_required" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="acknowledgement_deadline" type="TIMESTAMP WITH TIME ZONE"/>
<column name="acknowledged" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="acknowledged_on" type="TIMESTAMP WITH TIME ZONE"/>
</addColumn>
</changeSet>
<changeSet id="0405-inward-item-npci-package" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_inward_clearing_item"/>
<not><columnExists tableName="m_inward_clearing_item" columnName="original_serial_number"/></not>
</and>
</preConditions>
<addColumn tableName="m_inward_clearing_item">
<column name="original_serial_number" type="VARCHAR(20)"/>
<column name="presenting_branch_code" type="VARCHAR(20)"/>
<column name="return_deadline" type="DATE"/>
</addColumn>
</changeSet>
<changeSet id="0405-inward-acknowledge-permission" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'ACKNOWLEDGE_INWARDCLEARINGBATCH', 'INWARDCLEARINGBATCH', 'ACKNOWLEDGE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'ACKNOWLEDGE_INWARDCLEARINGBATCH');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Dedicated audit trail for every encrypt/sign/decrypt/verify hop in the clearing network (Bank-to-NPCI,
NPCI-to-Bank): which bank sent to which bank, which recipient key was targeted, whether the signature
verified, and where the plaintext + encrypted envelope were physically written on disk (via the same
ContentStoreService used for cheque images) — so every crypto operation is independently inspectable,
not just visible transiently in the JSON columns on the outward/inward rows themselves.
-->
<changeSet id="0406-clearing-crypto-audit" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_clearing_crypto_audit"/></not>
</preConditions>
<createTable tableName="m_clearing_crypto_audit">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="sender_role" type="VARCHAR(10)"><constraints nullable="false"/></column>
<column name="sender_bank_id" type="BIGINT"/>
<column name="sender_bank_name" type="VARCHAR(150)"/>
<column name="recipient_role" type="VARCHAR(10)"><constraints nullable="false"/></column>
<column name="recipient_bank_id" type="BIGINT"/>
<column name="recipient_bank_name" type="VARCHAR(150)"/>
<column name="recipient_key_id" type="VARCHAR(100)"/>
<column name="session_id" type="VARCHAR(50)"/>
<column name="envelope_type" type="VARCHAR(30)"/>
<column name="related_entity_type" type="VARCHAR(30)"><constraints nullable="false"/></column>
<column name="related_entity_id" type="BIGINT"/>
<column name="signature_valid" type="BOOLEAN"/>
<column name="plaintext_file_path" type="VARCHAR(500)"/>
<column name="encrypted_file_path" type="VARCHAR(500)"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"><constraints nullable="false"/></column>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Reverses the in-process NPCI/multi-bank crypto simulation added in 0390/0395/0398/0399/0405/0406.
This CBS will talk to a genuinely separate NPCI/peer-bank service later (real network call, its own
key custody); simulating every role in one process was demo-only scaffolding and is removed here.
Real, non-crypto behaviour is kept: the outward CTS-2010 file (cts_file_json), the descriptive
presenting-bank fields, and RBI T+1 return TAT tracking (return_tat_deadline / return_deadline).
Cannot edit the already-applied 0390/0395/0398/0399/0405/0406 changesets, so this is a new,
additive-but-reversing migration instead.
-->
<!--
NACH's m_nach_mandate/m_nach_batch have their own, unrelated "sponsor bank" concept (a plain Long
sponsor_bank_id column, no JPA relation — see NachMandate.java/NachBatch.java) whose DB-level FK
happened to point at m_clearing_bank. That NACH feature is out of scope here and stays untouched;
only the FK constraints (which would otherwise block dropping m_clearing_bank) are removed.
-->
<changeSet id="0407-drop-nach-sponsor-bank-fks" author="CBS">
<preConditions onFail="MARK_RAN">
<foreignKeyConstraintExists foreignKeyTableName="m_nach_mandate" foreignKeyName="fk_nach_mandate_sponsor"/>
</preConditions>
<dropForeignKeyConstraint baseTableName="m_nach_mandate" constraintName="fk_nach_mandate_sponsor"/>
</changeSet>
<changeSet id="0407-drop-nach-batch-sponsor-bank-fk" author="CBS">
<preConditions onFail="MARK_RAN">
<foreignKeyConstraintExists foreignKeyTableName="m_nach_batch" foreignKeyName="fk_nach_batch_sponsor"/>
</preConditions>
<dropForeignKeyConstraint baseTableName="m_nach_batch" constraintName="fk_nach_batch_sponsor"/>
</changeSet>
<changeSet id="0407-drop-clearing-bank-table" author="CBS">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_clearing_bank"/>
</preConditions>
<dropTable tableName="m_clearing_bank"/>
</changeSet>
<changeSet id="0407-drop-clearing-crypto-audit-table" author="CBS">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_clearing_crypto_audit"/>
</preConditions>
<dropTable tableName="m_clearing_crypto_audit"/>
</changeSet>
<changeSet id="0407-drop-outward-crypto-columns" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_outward_clearing_presentment"/>
<columnExists tableName="m_outward_clearing_presentment" columnName="outward_envelope"/>
</and>
</preConditions>
<dropColumn tableName="m_outward_clearing_presentment">
<column name="outward_envelope"/>
<column name="routed_to_bank"/>
<column name="npci_signature_valid"/>
<column name="npci_inward_package_json"/>
<column name="npci_inward_envelope"/>
</dropColumn>
</changeSet>
<changeSet id="0407-drop-inward-batch-crypto-columns" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_inward_clearing_batch"/>
<columnExists tableName="m_inward_clearing_batch" columnName="inward_envelope"/>
</and>
</preConditions>
<dropColumn tableName="m_inward_clearing_batch">
<column name="inward_envelope"/>
<column name="npci_processing_reference"/>
<column name="inward_package_json"/>
<column name="npci_signature_valid"/>
<column name="acknowledgement_required"/>
<column name="acknowledgement_deadline"/>
<column name="acknowledged"/>
<column name="acknowledged_on"/>
</dropColumn>
</changeSet>
<changeSet id="0407-remove-clearing-crypto-permissions" author="CBS" runOnChange="false">
<sql>
DELETE FROM m_permission WHERE code IN (
'READ_CLEARINGBANK', 'CREATE_CLEARINGBANK', 'BOOTSTRAP_CLEARINGBANK', 'ROTATEKEYS_CLEARINGBANK',
'ACKNOWLEDGE_INWARDCLEARINGBATCH'
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
This CBS's own RSA key pair for talking to the real, separate NPCI clearing-house service (a genuine
second process, not the deleted in-CBS simulation). Singleton row, generated once on first use.
private_key_pem never leaves this process; only public_key_pem is ever sent to NPCI, via bank
self-registration (POST /banks on the NPCI service).
-->
<changeSet id="0408-clearing-own-identity" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_clearing_own_identity"/></not>
</preConditions>
<createTable tableName="m_clearing_own_identity">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="public_key_pem" type="TEXT">
<constraints nullable="false"/>
</column>
<column name="private_key_pem" type="TEXT">
<constraints nullable="false"/>
</column>
<column name="registered_with_npci" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Stores NPCI's real routing response (signatureValid, routedTo, unroutable) for each outward batch,
from the actual, separate NPCI clearing-house service — for audit/display alongside the existing
cts_file_json column. Additive; safe no-op where already applied.
-->
<changeSet id="0409-outward-npci-response" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_outward_clearing_presentment"/>
<not><columnExists tableName="m_outward_clearing_presentment" columnName="npci_response_json"/></not>
</and>
</preConditions>
<addColumn tableName="m_outward_clearing_presentment">
<column name="npci_response_json" type="TEXT"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 package the real, separate NPCI clearing-house service has pushed to this CBS's
inward-callback endpoint (POST /api/v1/{tenant}/npci/inward-push) — decrypted with this bank's own
private key, NPCI's signature verified against NPCI's public key, kept here for manual review.
Deliberately does not auto-create an InwardClearingBatch; inward stays a manual, reviewed step.
-->
<changeSet id="0410-npci-inward-push-log" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_npci_inward_push_log"/></not>
</preConditions>
<createTable tableName="m_npci_inward_push_log">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="npci_package_id" type="BIGINT"/>
<column name="source_bank_code" type="VARCHAR(20)"/>
<column name="destination_bank_code" type="VARCHAR(20)"/>
<column name="source_batch_reference" type="VARCHAR(100)"/>
<column name="signature_valid" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="decrypted_package_json" type="TEXT"/>
<column name="received_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
A few descriptive fields carried on each CTS instrument from the depositing/sending side that were
previously only preserved in the raw NPCI push audit log (m_npci_inward_push_log.decrypted_package_json)
and not surfaced as their own columns/UI on the inward item itself. Additive; safe no-op where applied.
-->
<changeSet id="0411-inward-item-source-details" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_inward_clearing_item"/>
<not><columnExists tableName="m_inward_clearing_item" columnName="cts_reference_number"/></not>
</and>
</preConditions>
<addColumn tableName="m_inward_clearing_item">
<column name="cts_reference_number" type="VARCHAR(50)"/>
<column name="instrument_stop_payment_flag" type="BOOLEAN"/>
<column name="deposit_date" type="DATE"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--
Outward clearing is now a real session-batched flow (see the "Outward Clearing Batches" screen):
cheques no longer send individually, they wait for the next 2-hourly clearing session. The
"CTS Outward Clearing Batch Send" job (seeded inactive in 0403_outward_clearing_batch_job.xml) IS
that 2-hourly session scheduler (cron "0 0 10-18/2 * * ?"), so it now needs to actually run.
The manual "Send Pending Batch" button remains as a fallback if the scheduler has a problem.
-->
<changeSet author="CBS" id="0412_activate_outward_clearing_batch_job">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="1">SELECT COUNT(*) FROM job WHERE name = 'CTS Outward Clearing Batch Send' AND is_active = false</sqlCheck>
</preConditions>
<update tableName="job">
<column name="is_active" valueBoolean="true"/>
<where>name = 'CTS Outward Clearing Batch Send'</where>
</update>
</changeSet>
</databaseChangeLog>