feat(chequebook): add cheque book module with Maker-Checker workflow

Adds cheque series, stock lot/transfer, cheque book issuance,
stop-payment/lost/cancel-leaf actions, and pending-request approval
APIs with Maker-Checker command handlers and liquibase changelogs.
This commit is contained in:
Jaanu423
2026-07-15 11:13:06 +05:30
parent 9d09d9ebd4
commit 04d65d857f
85 changed files with 6146 additions and 3 deletions
@@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966}
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
@@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
@@ -689,4 +689,4 @@ fineract.email.from-name=${FINERACT_EMAIL_FROM_NAME:CBSnexora}
fineract.email.support-address=${FINERACT_EMAIL_SUPPORT_ADDRESS:support@cbsnexora.com}
# Frontend base URL — used for email links (voting, etc.)
cbs.frontend.base-url=${CBS_FRONTEND_BASE_URL:http://localhost:4200}
cbs.frontend.base-url=${CBS_FRONTEND_BASE_URL:https://cbs.godemo.in}
@@ -399,4 +399,13 @@
<include file="parts/0340_add_terminate_true_to_savings_and_current_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0368_catchup_savings_account_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0369_catchup_saving_product_temp_table.xml" relativeToChangelogFile="true"/>
<include file="parts/0379_chequebook_module.xml" relativeToChangelogFile="true"/>
<include file="parts/0380_cheque_series_account_type.xml" relativeToChangelogFile="true"/>
<include file="parts/0381_chequebook_no_of_books.xml" relativeToChangelogFile="true"/>
<include file="parts/0382_chequeleaf_used_status.xml" relativeToChangelogFile="true"/>
<include file="parts/0383_cheque_series_no_both.xml" relativeToChangelogFile="true"/>
<include file="parts/0384_chequebook_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0385_chequebook_pending_requests.xml" relativeToChangelogFile="true"/>
<include file="parts/0386_enable_global_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0387_chequeleaf_serial_unique_per_lot.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,287 @@
<?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 Book Inventory module: stationery master, a unified stock ledger (HO receipt and branch
transfer-in are the same "range of leaf serials sitting at an office, with a drawdown cursor"
concept), issuance to an account (Savings or Current — both exist in this codebase and are not
interchangeable, so a discriminator column is used instead of a JPA relation), and per-leaf
tracking (materialized at issuance, OR directly against a stock lot when unissued stock is
reported lost/stolen — see m_cheque_leaf.cheque_book_id being nullable).
Scope is deliberately limited to inventory + stop-payment/hotlisting. It does not model RBI's
Positive Pay System, "used/cleared" leaf status, or automatic cheque-validity staleness — all three
require clearing/presentment integration this CBS does not have; see the design plan for why
faking any of them would be actively wrong rather than merely incomplete.
Every changeSet is guarded so this migration is a safe no-op where already applied.
-->
<changeSet id="0379-cheque-series" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_series"/></not>
</preConditions>
<createTable tableName="m_cheque_series">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="series_code" type="VARCHAR(20)">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_cheque_series_code"/>
</column>
<column name="name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="leaves_per_book" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="cts_compliant" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="reorder_level" type="INTEGER"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet id="0379-cheque-stock-lot" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_stock_lot"/></not>
</preConditions>
<createTable tableName="m_cheque_stock_lot">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="series_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_lot_series" references="m_cheque_series(id)"/>
</column>
<column name="office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_lot_office" references="m_office(id)"/>
</column>
<column name="range_start" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="range_end" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="next_serial_no" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="source" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="source_ref_id" type="BIGINT"/>
<column name="batch_ref" type="VARCHAR(100)"/>
<column name="status" type="VARCHAR(25)" defaultValue="AVAILABLE">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="received_by" type="BIGINT"/>
<column name="received_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
<changeSet id="0379-cheque-stock-transfer" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_stock_transfer"/></not>
</preConditions>
<createTable tableName="m_cheque_stock_transfer">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="series_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_series" references="m_cheque_series(id)"/>
</column>
<column name="from_office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_from_office" references="m_office(id)"/>
</column>
<column name="to_office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_to_office" references="m_office(id)"/>
</column>
<column name="from_lot_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_from_lot" references="m_cheque_stock_lot(id)"/>
</column>
<column name="to_lot_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_to_lot" references="m_cheque_stock_lot(id)"/>
</column>
<column name="range_start" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="range_end" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="total_leaves" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="dispatch_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(20)" defaultValue="DISPATCHED">
<constraints nullable="false"/>
</column>
<column name="received_date" type="DATE"/>
<column name="dispatched_by" type="BIGINT"/>
<column name="received_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet id="0379-cheque-book" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_book"/></not>
</preConditions>
<createTable tableName="m_cheque_book">
<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="office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_book_office" references="m_office(id)"/>
</column>
<column name="series_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_cheque_book_series" references="m_cheque_series(id)"/>
</column>
<column name="requested_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="requested_by" type="BIGINT"/>
<column name="no_of_leaves_requested" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(20)" defaultValue="REQUESTED">
<constraints nullable="false"/>
</column>
<column name="issue_date" type="DATE"/>
<column name="issued_by" type="BIGINT"/>
<column name="lot_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_cheque_book_lot" references="m_cheque_stock_lot(id)"/>
</column>
<column name="range_start" type="BIGINT"/>
<column name="range_end" type="BIGINT"/>
<column name="total_leaves" type="INTEGER"/>
<column name="rejection_reason" type="VARCHAR(500)"/>
<column name="collection_acknowledged" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="collected_date" type="DATE"/>
<column name="collected_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
<changeSet id="0379-cheque-leaf" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_leaf"/></not>
</preConditions>
<createTable tableName="m_cheque_leaf">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="cheque_book_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_cheque_leaf_book" references="m_cheque_book(id)"/>
</column>
<column name="lot_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_leaf_lot" references="m_cheque_stock_lot(id)"/>
</column>
<column name="leaf_serial_no" type="BIGINT">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_cheque_leaf_serial_no"/>
</column>
<column name="leaf_seq_no" type="INTEGER"/>
<column name="status" type="VARCHAR(20)" defaultValue="ACTIVE">
<constraints nullable="false"/>
</column>
<column name="stop_reason" type="VARCHAR(500)"/>
<column name="stopped_by" type="BIGINT"/>
<column name="stopped_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="lost_reason" type="VARCHAR(500)"/>
<column name="lost_reported_by" type="BIGINT"/>
<column name="lost_reported_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"/>
</createTable>
</changeSet>
<changeSet id="0379-permissions" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CHEQUESERIES', 'CHEQUESERIES', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CHEQUESERIES');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CHEQUESERIES', 'CHEQUESERIES', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CHEQUESERIES');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'UPDATE_CHEQUESERIES', 'CHEQUESERIES', 'UPDATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'UPDATE_CHEQUESERIES');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'DELETE_CHEQUESERIES', 'CHEQUESERIES', 'DELETE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'DELETE_CHEQUESERIES');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CHEQUESTOCKLOT', 'CHEQUESTOCKLOT', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CHEQUESTOCKLOT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CHEQUESTOCKLOT', 'CHEQUESTOCKLOT', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CHEQUESTOCKLOT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REPORTLOST_CHEQUESTOCKLOT', 'CHEQUESTOCKLOT', 'REPORTLOST', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REPORTLOST_CHEQUESTOCKLOT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CHEQUESTOCKTRANSFER', 'CHEQUESTOCKTRANSFER', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CHEQUESTOCKTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'RECEIVE_CHEQUESTOCKTRANSFER', 'CHEQUESTOCKTRANSFER', 'RECEIVE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'RECEIVE_CHEQUESTOCKTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REJECT_CHEQUESTOCKTRANSFER', 'CHEQUESTOCKTRANSFER', 'REJECT', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_CHEQUESTOCKTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CHEQUEBOOK', 'CHEQUEBOOK', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CHEQUEBOOK', 'CHEQUEBOOK', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'ISSUE_CHEQUEBOOK', 'CHEQUEBOOK', 'ISSUE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'ISSUE_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REJECT_CHEQUEBOOK', 'CHEQUEBOOK', 'REJECT', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'WITHDRAW_CHEQUEBOOK', 'CHEQUEBOOK', 'WITHDRAW', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'WITHDRAW_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CANCELLEAF_CHEQUEBOOK', 'CHEQUEBOOK', 'CANCELLEAF', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CANCELLEAF_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'STOPPAYMENT_CHEQUEBOOK', 'CHEQUEBOOK', 'STOPPAYMENT', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'STOPPAYMENT_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REVOKESTOPPAYMENT_CHEQUEBOOK', 'CHEQUEBOOK', 'REVOKESTOPPAYMENT', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVOKESTOPPAYMENT_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REPORTLOST_CHEQUEBOOK', 'CHEQUEBOOK', 'REPORTLOST', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REPORTLOST_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'heading_menu', 'MENU_CHEQUEBOOK', 'MENU', 'CHEQUEBOOK', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'MENU_CHEQUEBOOK');
</sql>
</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">
<!--
Segregate cheque stationery by account scheme (Savings vs Current). Real banks stock physically
different cheque books for SB and CA accounts, so each series is tagged SAVINGS / CURRENT / BOTH.
Existing rows default to BOTH (backward compatible: any series stays issuable to any account type
until an operator narrows it). Issuance is validated against this at issue time.
-->
<changeSet id="0380-cheque-series-applicable-account-type" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_cheque_series" columnName="applicable_account_type"/></not>
</preConditions>
<addColumn tableName="m_cheque_series">
<column name="applicable_account_type" type="VARCHAR(10)" defaultValue="BOTH">
<constraints nullable="false"/>
</column>
</addColumn>
</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 books are requested and issued in whole BOOKS of a fixed denomination (25-leaf, 50-leaf, ...),
not in an arbitrary leaf count. A requisition captures how many books the customer wants; the actual
leaves handed over are derived at issue time as books x series.leavesPerBook, and that leaf count plus
the per-leaf serials are persisted. no_of_leaves_requested becomes optional/legacy.
-->
<changeSet id="0381-cheque-book-no-of-books" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_cheque_book" columnName="no_of_books"/></not>
</preConditions>
<addColumn tableName="m_cheque_book">
<column name="no_of_books" type="INTEGER" defaultValueNumeric="1">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0381-cheque-book-leaves-requested-optional" author="CBS">
<preConditions onFail="MARK_RAN">
<columnExists tableName="m_cheque_book" columnName="no_of_leaves_requested"/>
</preConditions>
<dropNotNullConstraint tableName="m_cheque_book" columnName="no_of_leaves_requested" columnDataType="INTEGER"/>
</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">
<!--
A manually-recorded USED status for a cheque leaf. This CBS has no clearing/presentment integration,
so the system cannot detect on its own when a specific leaf was honoured — a branch marks it USED
after seeing it clear on the bank/clearing statement (the same manual-recording pattern already used
for the settlement module's "actual bank settlement" entry, where automatic detection isn't feasible
either). This is a record-keeping status, not a fraud control, so it is not maker-checker.
-->
<changeSet id="0382-cheque-leaf-used-columns" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_cheque_leaf" columnName="used_reason"/></not>
</preConditions>
<addColumn tableName="m_cheque_leaf">
<column name="used_reason" type="VARCHAR(500)"/>
<column name="used_by" type="BIGINT"/>
<column name="used_on" type="TIMESTAMP WITH TIME ZONE"/>
</addColumn>
</changeSet>
<changeSet id="0382-permission" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'MARKUSED_CHEQUEBOOK', 'CHEQUEBOOK', 'MARKUSED', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'MARKUSED_CHEQUEBOOK');
</sql>
</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">
<!--
"BOTH" is removed as a cheque series applicability: every series must now be tagged explicitly as
SAVINGS or CURRENT (real banks stock physically different stationery per scheme, so "applies to
everything" was never a genuine stationery category). Any pre-existing BOTH-tagged row is narrowed
to SAVINGS — an operator can re-create a CURRENT-tagged series if that stock is also needed there.
-->
<changeSet id="0383-cheque-series-no-both-data" author="CBS" runOnChange="false">
<sql>
UPDATE m_cheque_series SET applicable_account_type = 'SAVINGS' WHERE applicable_account_type = 'BOTH';
</sql>
</changeSet>
<changeSet id="0383-cheque-series-no-both-default" author="CBS">
<preConditions onFail="MARK_RAN">
<columnExists tableName="m_cheque_series" columnName="applicable_account_type"/>
</preConditions>
<dropDefaultValue tableName="m_cheque_series" columnName="applicable_account_type"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,21 @@
<?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">
<!--
Extend maker-checker to the three cheque-book creation actions (series master, stock receipt, book
requisition) so every request now needs a second user's approval before it takes effect, matching
the fraud-sensitive actions (stop-payment/report-lost/revoke) that already required it. No bespoke
approval logic is needed - can_maker_checker=true on the permission row is all the generic
command-processing pipeline needs; pending requests surface in the existing Checker Inbox screen.
-->
<changeSet id="0384-chequebook-maker-checker" author="CBS" runOnChange="false">
<sql>
UPDATE m_permission SET can_maker_checker = true
WHERE code IN ('CREATE_CHEQUESERIES', 'CREATE_CHEQUESTOCKLOT', 'CREATE_CHEQUEBOOK');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,77 @@
<?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">
<!--
Bespoke maker-checker for the three cheque-book creation actions (series master, stock receipt, book
requisition), mirroring the existing m_clerk_disbursement_request pattern rather than the native
Fineract command-approval engine: the native engine's maker-checker hold is bypassed by any user
whose role carries ALL_FUNCTIONS (Super user, Clerk in this tenant), which made it useless for this
CBS's actual admin accounts. This table instead holds a request as PENDING with its raw request JSON;
approval replays that JSON through the same creation logic used elsewhere in this module. The
submitting user (maker) can never approve/reject their own request (enforced in code, not just by
role) - any OTHER user with the same create permission can act as checker.
-->
<changeSet id="0385-chequebook-pending-requests-table" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_chequebook_pending_request"/></not>
</preConditions>
<createTable tableName="m_chequebook_pending_request">
<column name="id" type="BIGSERIAL">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="request_type" type="VARCHAR(30)">
<constraints nullable="false"/>
</column>
<column name="payload_json" type="TEXT">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(20)" defaultValue="PENDING">
<constraints nullable="false"/>
</column>
<column name="maker_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="maker_name" type="VARCHAR(150)"/>
<column name="maker_username" type="VARCHAR(100)"/>
<column name="submitted_on" type="TIMESTAMP WITH TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="checker_id" type="BIGINT"/>
<column name="checker_name" type="VARCHAR(150)"/>
<column name="checker_username" type="VARCHAR(100)"/>
<column name="checked_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="rejection_reason" type="VARCHAR(500)"/>
<column name="created_resource_id" type="BIGINT"/>
<column name="activity_log" type="TEXT"/>
</createTable>
<sql>CREATE INDEX IF NOT EXISTS idx_cbpr_status ON m_chequebook_pending_request(status)</sql>
<sql>CREATE INDEX IF NOT EXISTS idx_cbpr_maker_id ON m_chequebook_pending_request(maker_id)</sql>
<sql>CREATE INDEX IF NOT EXISTS idx_cbpr_submitted_on ON m_chequebook_pending_request(submitted_on)</sql>
</changeSet>
<changeSet id="0385-chequebook-revert-native-maker-checker" author="CBS" runOnChange="false">
<!-- the native engine's hold is replaced by this table's bespoke workflow for these three actions -->
<sql>
UPDATE m_permission SET can_maker_checker = false
WHERE code IN ('CREATE_CHEQUESERIES', 'CREATE_CHEQUESTOCKLOT', 'CREATE_CHEQUEBOOK');
</sql>
</changeSet>
<changeSet id="0385-chequebook-pending-request-permission" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CHEQUEBOOKPENDINGREQUEST', 'CHEQUEBOOKPENDINGREQUEST', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CHEQUEBOOKPENDINGREQUEST');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'APPROVE_CHEQUEBOOKPENDINGREQUEST', 'CHEQUEBOOKPENDINGREQUEST', 'APPROVE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'APPROVE_CHEQUEBOOKPENDINGREQUEST');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REJECT_CHEQUEBOOKPENDINGREQUEST', 'CHEQUEBOOKPENDINGREQUEST', 'REJECT', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_CHEQUEBOOKPENDINGREQUEST');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,23 @@
<?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">
<!--
Fineract holds a per-permission can_maker_checker flag AND a single global "maker-checker" switch in
c_configuration - the global switch is checked FIRST (ConfigurationDomainServiceJpa.isMakerCheckerEnabledForTask),
and if it is disabled, every maker-checker-eligible permission in the whole system is bypassed
regardless of its own flag. It shipped disabled in this tenant, which meant the fraud-sensitive
actions built earlier (stop-payment/report-lost/revoke) were never actually holding for approval
either. Enabling it here turns on real maker-checker enforcement platform-wide, not just for the
cheque-book module. NB: a user whose role carries ALL_FUNCTIONS or CHECKER_SUPER_USER (Super user,
Clerk in this tenant) still bypasses the hold regardless of this switch - see AppUser.isCheckerSuperUser().
-->
<changeSet id="0386-enable-global-maker-checker" author="CBS" runOnChange="false">
<sql>
UPDATE c_configuration SET enabled = true WHERE name = 'maker-checker';
</sql>
</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">
<!--
leaf_serial_no was originally unique bank-wide, which matched the OLD global stock-numbering
design. Per-series numbering (0383) intentionally lets two different series share a serial number
(each is a separate stationery order from the press, numbered independently) - so a bare
leaf_serial_no can no longer be unique on its own once real leaves are materialized for two series
that happen to overlap. Uniqueness now needs to be scoped to the stock lot a leaf was drawn from
(every lot belongs to exactly one series, and lots within one series never overlap), not the whole
table.
-->
<changeSet id="0387-chequeleaf-serial-unique-per-lot" author="CBS" runOnChange="false">
<preConditions onFail="MARK_RAN">
<indexExists indexName="uk_cheque_leaf_serial_no"/>
</preConditions>
<dropUniqueConstraint tableName="m_cheque_leaf" constraintName="uk_cheque_leaf_serial_no"/>
</changeSet>
<changeSet id="0387-chequeleaf-serial-unique-per-lot-add" author="CBS" runOnChange="false">
<preConditions onFail="MARK_RAN">
<not>
<indexExists indexName="uk_cheque_leaf_lot_serial_no"/>
</not>
</preConditions>
<addUniqueConstraint tableName="m_cheque_leaf" columnNames="lot_id, leaf_serial_no"
constraintName="uk_cheque_leaf_lot_serial_no"/>
</changeSet>
</databaseChangeLog>