Compare commits
98
Commits
f18b5d97d3
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af43944699 | ||
|
|
39fa100c70 | ||
|
|
1c6ac9d77f | ||
|
|
6bb512e1fe | ||
|
|
04ac233fbe | ||
|
|
9e0c04583d | ||
|
|
c20a7d0728 | ||
|
|
3d196276c8 | ||
|
|
5cc5d680f2 | ||
|
|
6aa655a8a3 | ||
|
|
518b4d4afc | ||
|
|
05bceea2d4 | ||
|
|
e33e0f23fe | ||
|
|
317a308d8c | ||
|
|
a83806062e | ||
|
|
e4cc2e558e | ||
|
|
a04b65583e | ||
|
|
f6686b0c8f | ||
|
|
45c2826a42 | ||
|
|
c86e0d1cc2 | ||
|
|
c052beeacf | ||
|
|
942bad6c8c | ||
|
|
5a9a103832 | ||
|
|
fd91da3d76 | ||
|
|
c4a99fea5d | ||
|
|
dc0539d53d | ||
|
|
509370eaeb | ||
|
|
1130a482a0 | ||
|
|
8ecab08d57 | ||
|
|
29677e610c | ||
|
|
6ffb0dd9c0 | ||
|
|
8d224a22b4 | ||
|
|
f746a1fb2c | ||
|
|
1bc9d0901d | ||
|
|
08519da8b1 | ||
|
|
58187c23d0 | ||
|
|
179fe3a6ce | ||
|
|
72221f704d | ||
|
|
a42a874811 | ||
|
|
cad7f54c47 | ||
|
|
1a2a945464 | ||
|
|
73ec85cfd6 | ||
|
|
46f5a1babd | ||
|
|
20fe11e60d | ||
|
|
eb1aa767f7 | ||
|
|
8724c54d9b | ||
|
|
5c0a482f49 | ||
|
|
0e07e60328 | ||
|
|
e227e8ae0f | ||
|
|
231919b62b | ||
|
|
9eac40bb5c | ||
|
|
14b2585cf5 | ||
|
|
a2fa8688ae | ||
|
|
a8ca7dd948 | ||
|
|
c8833367b9 | ||
|
|
94b0afff38 | ||
|
|
9dd4f8bedb | ||
|
|
1107610c08 | ||
|
|
2213308254 | ||
|
|
c568eb0abf | ||
|
|
1426be9531 | ||
|
|
541c6ab062 | ||
|
|
66a6a4766f | ||
|
|
b9aa29fb35 | ||
|
|
1a9adaec24 | ||
|
|
2da5a25e04 | ||
|
|
baf30cff68 | ||
|
|
f1d68d17fe | ||
|
|
84f8aa2274 | ||
|
|
7773a9e9c6 | ||
|
|
c890faee78 | ||
|
|
abc7eb22a2 | ||
|
|
d7d22382a5 | ||
|
|
8622dcaf90 | ||
|
|
7cdc25322e | ||
|
|
a4c3dbb6c4 | ||
|
|
dd8445de06 | ||
|
|
9345f2294c | ||
|
|
190cd92a85 | ||
|
|
81832b6bb7 | ||
|
|
0bd5296519 | ||
|
|
17e0dab435 | ||
|
|
67810bfaea | ||
|
|
2022dc5241 | ||
|
|
b1cfa996b4 | ||
|
|
02541eed6c | ||
|
|
7c15fbb0c7 | ||
|
|
6e3f195de0 | ||
|
|
a65d801250 | ||
|
|
f45d5890ea | ||
|
|
3c58bb231f | ||
|
|
aabda18b69 | ||
|
|
0e5671b706 | ||
|
|
67214ec0f8 | ||
|
|
026d902199 | ||
|
|
bc30241950 | ||
|
|
14df914b9a | ||
|
|
5dc175c5e1 |
+1
-1
@@ -159,7 +159,7 @@ gitVersioning.apply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ext['groovy.version'] = '4.0.17'
|
ext['groovy.version'] = '4.0.17'
|
||||||
ext['swaggerFile'] = "$rootDir/fineract-provider/build/resources/main/static/fineract.json".toString()
|
ext['swaggerFile'] = "$rootDir/fineract-provider/build/resources/main/static/fineract.json".toString().replace(" ", "%20")
|
||||||
|
|
||||||
// Apply static weaving configuration to all subprojects
|
// Apply static weaving configuration to all subprojects
|
||||||
subprojects { subproject ->
|
subprojects { subproject ->
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
-- Removes the duplicate vouchers left behind by the double-count bug.
|
||||||
|
--
|
||||||
|
-- Until 2026-09-03 the cashier deposit and clerk withdrawal registers did not wrap their inner
|
||||||
|
-- savings command in VoucherService.withOwnVoucher(...), so one counter transaction raised two
|
||||||
|
-- vouchers over the same money: the register's own (CASHIER_DEPOSIT_REQUEST / CLERK_WITHDRAWAL_
|
||||||
|
-- REQUEST) and a second one from the savings command (SAVINGS_TRANSACTION). The cashier's Received
|
||||||
|
-- and Payment reports counted both, doubling the day's cash.
|
||||||
|
--
|
||||||
|
-- The code is fixed, so no new duplicates are created. This clears the ones already recorded.
|
||||||
|
--
|
||||||
|
-- WHICH ONE IS DELETED: the SAVINGS_TRANSACTION voucher. The register's voucher is the keeper --
|
||||||
|
-- it is the one the register owns, attaches its DR/CR lines to, and stamps onto
|
||||||
|
-- m_savings_account_transaction.voucher_no. Deleting the register's voucher instead would break
|
||||||
|
-- that link and orphan the request row.
|
||||||
|
--
|
||||||
|
-- Run: psql -h localhost -U postgres -d fineract_default -v ON_ERROR_STOP=1 -f cleanup_duplicate_vouchers.sql
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- The duplicates: a SAVINGS_TRANSACTION voucher whose transaction is already claimed by a
|
||||||
|
-- register voucher. Matching through savings_txn_id rather than by date or amount so that two
|
||||||
|
-- genuinely separate transactions of the same value on the same day are never confused.
|
||||||
|
CREATE TEMP TABLE dup_vouchers ON COMMIT DROP AS
|
||||||
|
SELECT sav.voucher_no
|
||||||
|
FROM m_voucher sav
|
||||||
|
JOIN m_voucher reg
|
||||||
|
ON reg.savings_txn_id = sav.reference_id
|
||||||
|
AND reg.reference_type IN ('CLERK_WITHDRAWAL_REQUEST', 'CASHIER_DEPOSIT_REQUEST')
|
||||||
|
WHERE sav.reference_type = 'SAVINGS_TRANSACTION'
|
||||||
|
AND sav.voucher_type IN ('DEPOSIT', 'WITHDRAWAL');
|
||||||
|
|
||||||
|
-- Refuse to run if anything else has attached itself to a voucher about to be deleted: a
|
||||||
|
-- verification or an attachment means someone acted on that slip, and it must be looked at by hand
|
||||||
|
-- rather than removed silently.
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
blocked INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO blocked
|
||||||
|
FROM (SELECT voucher_no FROM m_voucher_verification
|
||||||
|
WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers)
|
||||||
|
UNION ALL
|
||||||
|
SELECT voucher_no FROM m_voucher_attachment
|
||||||
|
WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers)) x;
|
||||||
|
IF blocked > 0 THEN
|
||||||
|
RAISE EXCEPTION 'Aborting: % duplicate voucher(s) carry a verification or attachment. Review these by hand.', blocked;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Likewise refuse if a savings transaction is stamped with a voucher about to be deleted. It should
|
||||||
|
-- carry the register's number, not the duplicate's; if it does not, the pair needs looking at.
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
stamped INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO stamped FROM m_savings_account_transaction
|
||||||
|
WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers);
|
||||||
|
IF stamped > 0 THEN
|
||||||
|
RAISE EXCEPTION 'Aborting: % savings transaction(s) are stamped with a duplicate voucher number.', stamped;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
SELECT voucher_no AS "about to delete" FROM dup_vouchers ORDER BY voucher_no;
|
||||||
|
|
||||||
|
DELETE FROM m_voucher_line WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers);
|
||||||
|
DELETE FROM m_voucher WHERE voucher_no IN (SELECT voucher_no FROM dup_vouchers);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -127,14 +127,6 @@ SELECT 'Inter-Bank Cap Charge', 'INR', 0.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
|||||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Inter-Bank Cap Charge' AND is_deleted = 0);
|
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Inter-Bank Cap Charge' AND is_deleted = 0);
|
||||||
|
|
||||||
INSERT IGNORE INTO m_charge
|
|
||||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
|
||||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
|
||||||
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
|
|
||||||
SELECT 'Withdrawal Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
|
|
||||||
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Withdrawal Facility Charge' AND is_deleted = 0);
|
|
||||||
|
|
||||||
INSERT IGNORE INTO m_charge
|
INSERT IGNORE INTO m_charge
|
||||||
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
|
||||||
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
charge_calculation_enum, is_penalty, is_active, is_deleted,
|
||||||
@@ -168,6 +160,5 @@ VALUES
|
|||||||
('NEFT_FACILITY', 'NEFT Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'NEFT Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
('NEFT_FACILITY', 'NEFT Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'NEFT Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||||
('RTGS_FACILITY', 'RTGS Facility Charge', 25.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'RTGS Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
('RTGS_FACILITY', 'RTGS Facility Charge', 25.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'RTGS Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
||||||
('INTER_BANK_TOTAL_CAP','Inter-Bank Cap Charge', 0.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Inter-Bank Cap Charge' AND is_deleted = 0 LIMIT 1)),
|
('INTER_BANK_TOTAL_CAP','Inter-Bank Cap Charge', 0.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Inter-Bank Cap Charge' AND is_deleted = 0 LIMIT 1)),
|
||||||
('WITHDRAWAL_FACILITY', 'Withdrawal Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Withdrawal Facility Charge' AND is_deleted = 0 LIMIT 1)),
|
|
||||||
('DEPOSIT_FACILITY_KYC','Deposit KYC Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Deposit KYC Charge' AND is_deleted = 0 LIMIT 1)),
|
('DEPOSIT_FACILITY_KYC','Deposit KYC Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Deposit KYC Charge' AND is_deleted = 0 LIMIT 1)),
|
||||||
('VIRTUAL_DEBIT_CARD', 'Virtual Debit Card Charge', 199.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Virtual Debit Card Charge' AND is_deleted = 0 LIMIT 1));
|
('VIRTUAL_DEBIT_CARD', 'Virtual Debit Card Charge', 199.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Virtual Debit Card Charge' AND is_deleted = 0 LIMIT 1));
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ ALTER TABLE m_current_product_facility_config
|
|||||||
ADD COLUMN IF NOT EXISTS neft_facility_charge_id BIGINT NULL,
|
ADD COLUMN IF NOT EXISTS neft_facility_charge_id BIGINT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS rtgs_facility_charge_id BIGINT NULL,
|
ADD COLUMN IF NOT EXISTS rtgs_facility_charge_id BIGINT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS inter_bank_total_cap_charge_id BIGINT NULL,
|
ADD COLUMN IF NOT EXISTS inter_bank_total_cap_charge_id BIGINT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS withdrawal_facility_charge_id BIGINT NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS deposit_facility_kyc_charge_id BIGINT NULL,
|
ADD COLUMN IF NOT EXISTS deposit_facility_kyc_charge_id BIGINT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS virtual_debit_card_charge_id BIGINT NULL;
|
ADD COLUMN IF NOT EXISTS virtual_debit_card_charge_id BIGINT NULL;
|
||||||
|
|
||||||
@@ -36,7 +35,6 @@ ALTER TABLE m_current_product_facility_config
|
|||||||
ADD CONSTRAINT fk_cpfc_neft_charge FOREIGN KEY (neft_facility_charge_id) REFERENCES m_charge(id),
|
ADD CONSTRAINT fk_cpfc_neft_charge FOREIGN KEY (neft_facility_charge_id) REFERENCES m_charge(id),
|
||||||
ADD CONSTRAINT fk_cpfc_rtgs_charge FOREIGN KEY (rtgs_facility_charge_id) REFERENCES m_charge(id),
|
ADD CONSTRAINT fk_cpfc_rtgs_charge FOREIGN KEY (rtgs_facility_charge_id) REFERENCES m_charge(id),
|
||||||
ADD CONSTRAINT fk_cpfc_ibtc_charge FOREIGN KEY (inter_bank_total_cap_charge_id) REFERENCES m_charge(id),
|
ADD CONSTRAINT fk_cpfc_ibtc_charge FOREIGN KEY (inter_bank_total_cap_charge_id) REFERENCES m_charge(id),
|
||||||
ADD CONSTRAINT fk_cpfc_wd_charge FOREIGN KEY (withdrawal_facility_charge_id) REFERENCES m_charge(id),
|
|
||||||
ADD CONSTRAINT fk_cpfc_dep_charge FOREIGN KEY (deposit_facility_kyc_charge_id) REFERENCES m_charge(id),
|
ADD CONSTRAINT fk_cpfc_dep_charge FOREIGN KEY (deposit_facility_kyc_charge_id) REFERENCES m_charge(id),
|
||||||
ADD CONSTRAINT fk_cpfc_vdc_charge FOREIGN KEY (virtual_debit_card_charge_id) REFERENCES m_charge(id);
|
ADD CONSTRAINT fk_cpfc_vdc_charge FOREIGN KEY (virtual_debit_card_charge_id) REFERENCES m_charge(id);
|
||||||
|
|
||||||
@@ -55,7 +53,6 @@ ALTER TABLE m_savings_product_facility_config
|
|||||||
ADD COLUMN IF NOT EXISTS neft_facility_charge_id BIGINT NULL,
|
ADD COLUMN IF NOT EXISTS neft_facility_charge_id BIGINT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS rtgs_facility_charge_id BIGINT NULL,
|
ADD COLUMN IF NOT EXISTS rtgs_facility_charge_id BIGINT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS inter_bank_total_cap_charge_id BIGINT NULL,
|
ADD COLUMN IF NOT EXISTS inter_bank_total_cap_charge_id BIGINT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS withdrawal_facility_charge_id BIGINT NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS deposit_facility_kyc_charge_id BIGINT NULL,
|
ADD COLUMN IF NOT EXISTS deposit_facility_kyc_charge_id BIGINT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS virtual_debit_card_charge_id BIGINT NULL;
|
ADD COLUMN IF NOT EXISTS virtual_debit_card_charge_id BIGINT NULL;
|
||||||
|
|
||||||
@@ -74,6 +71,5 @@ ALTER TABLE m_savings_product_facility_config
|
|||||||
ADD CONSTRAINT fk_spfc_neft_charge FOREIGN KEY (neft_facility_charge_id) REFERENCES m_charge(id),
|
ADD CONSTRAINT fk_spfc_neft_charge FOREIGN KEY (neft_facility_charge_id) REFERENCES m_charge(id),
|
||||||
ADD CONSTRAINT fk_spfc_rtgs_charge FOREIGN KEY (rtgs_facility_charge_id) REFERENCES m_charge(id),
|
ADD CONSTRAINT fk_spfc_rtgs_charge FOREIGN KEY (rtgs_facility_charge_id) REFERENCES m_charge(id),
|
||||||
ADD CONSTRAINT fk_spfc_ibtc_charge FOREIGN KEY (inter_bank_total_cap_charge_id) REFERENCES m_charge(id),
|
ADD CONSTRAINT fk_spfc_ibtc_charge FOREIGN KEY (inter_bank_total_cap_charge_id) REFERENCES m_charge(id),
|
||||||
ADD CONSTRAINT fk_spfc_wd_charge FOREIGN KEY (withdrawal_facility_charge_id) REFERENCES m_charge(id),
|
|
||||||
ADD CONSTRAINT fk_spfc_dep_charge FOREIGN KEY (deposit_facility_kyc_charge_id) REFERENCES m_charge(id),
|
ADD CONSTRAINT fk_spfc_dep_charge FOREIGN KEY (deposit_facility_kyc_charge_id) REFERENCES m_charge(id),
|
||||||
ADD CONSTRAINT fk_spfc_vdc_charge FOREIGN KEY (virtual_debit_card_charge_id) REFERENCES m_charge(id);
|
ADD CONSTRAINT fk_spfc_vdc_charge FOREIGN KEY (virtual_debit_card_charge_id) REFERENCES m_charge(id);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const https = require('https');
|
||||||
|
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
|
||||||
|
fetch('https://localhost:8443/fineract-provider/api/v1/authentication?username=mifos&password=password&tenantIdentifier=default', {
|
||||||
|
method: 'POST',
|
||||||
|
agent: agent
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
const token = data.base64EncodedAuthenticationKey;
|
||||||
|
return fetch('https://localhost:8443/fineract-provider/api/v1/clients/9/accounts?tenantIdentifier=default', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Basic ' + token,
|
||||||
|
'Fineract-Platform-TenantId': 'default'
|
||||||
|
},
|
||||||
|
agent: agent
|
||||||
|
});
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
}).catch(console.error);
|
||||||
+20
-9
@@ -45,6 +45,22 @@ public final class FinancialActivityAccountDataValidator {
|
|||||||
*/
|
*/
|
||||||
private final Set<String> supportedParameters = FinancialActivityAccountsJsonInputParams.getAllValues();
|
private final Set<String> supportedParameters = FinancialActivityAccountsJsonInputParams.getAllValues();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activities an administrator may map from the Financial Activity Mappings screen.
|
||||||
|
*
|
||||||
|
* Held in one place because create and update both validate against it, and the two lists had already drifted -
|
||||||
|
* update omitted the two cash activities, and neither offered the dividend activities beyond PAYABLE_DIVIDENDS even
|
||||||
|
* though the accounting code reads them. An activity the posting code resolves but this list omits can only ever be
|
||||||
|
* seeded by a migration, which is how DIVIDEND_PAID and RESERVE_FOR_DIVIDEND came to exist without being editable.
|
||||||
|
*/
|
||||||
|
private static final Object[] MAPPABLE_FINANCIAL_ACTIVITIES = { FinancialActivity.ASSET_TRANSFER.getValue(),
|
||||||
|
FinancialActivity.LIABILITY_TRANSFER.getValue(), FinancialActivity.CASH_AT_MAINVAULT.getValue(),
|
||||||
|
FinancialActivity.CASH_AT_TELLER.getValue(), FinancialActivity.OPENING_BALANCES_TRANSFER_CONTRA.getValue(),
|
||||||
|
FinancialActivity.ASSET_FUND_SOURCE.getValue(), FinancialActivity.PAYABLE_DIVIDENDS.getValue(),
|
||||||
|
FinancialActivity.DIVIDEND_PAID.getValue(), FinancialActivity.RESERVE_FOR_DIVIDEND.getValue(),
|
||||||
|
FinancialActivity.DIVIDEND_FORFEITED_INCOME.getValue(), FinancialActivity.DISTRIBUTABLE_PROFIT.getValue(),
|
||||||
|
FinancialActivity.RETAINED_PROFIT.getValue(), FinancialActivity.DIVIDEND_ROUND_OFF.getValue() };
|
||||||
|
|
||||||
private final String paramNameForFinancialActivity = FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue();
|
private final String paramNameForFinancialActivity = FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue();
|
||||||
private final String paramNameForGLAccount = FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue();
|
private final String paramNameForGLAccount = FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue();
|
||||||
|
|
||||||
@@ -59,11 +75,8 @@ public final class FinancialActivityAccountDataValidator {
|
|||||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
|
||||||
final Integer financialActivityId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(paramNameForFinancialActivity, element);
|
final Integer financialActivityId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(paramNameForFinancialActivity, element);
|
||||||
baseDataValidator.reset().parameter(paramNameForFinancialActivity).value(financialActivityId).notNull().isOneOfTheseValues(
|
baseDataValidator.reset().parameter(paramNameForFinancialActivity).value(financialActivityId).notNull()
|
||||||
FinancialActivity.ASSET_TRANSFER.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(),
|
.isOneOfTheseValues(MAPPABLE_FINANCIAL_ACTIVITIES);
|
||||||
FinancialActivity.CASH_AT_MAINVAULT.getValue(), FinancialActivity.CASH_AT_TELLER.getValue(),
|
|
||||||
FinancialActivity.OPENING_BALANCES_TRANSFER_CONTRA.getValue(), FinancialActivity.ASSET_FUND_SOURCE.getValue(),
|
|
||||||
FinancialActivity.PAYABLE_DIVIDENDS.getValue());
|
|
||||||
|
|
||||||
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(paramNameForGLAccount, element);
|
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(paramNameForGLAccount, element);
|
||||||
baseDataValidator.reset().parameter(paramNameForGLAccount).value(glAccountId).notNull().integerGreaterThanZero();
|
baseDataValidator.reset().parameter(paramNameForGLAccount).value(glAccountId).notNull().integerGreaterThanZero();
|
||||||
@@ -86,10 +99,8 @@ public final class FinancialActivityAccountDataValidator {
|
|||||||
if (this.fromApiJsonHelper.parameterExists(paramNameForFinancialActivity, element)) {
|
if (this.fromApiJsonHelper.parameterExists(paramNameForFinancialActivity, element)) {
|
||||||
final Integer financialActivityId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(paramNameForFinancialActivity,
|
final Integer financialActivityId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(paramNameForFinancialActivity,
|
||||||
element);
|
element);
|
||||||
baseDataValidator.reset().parameter(paramNameForFinancialActivity).value(financialActivityId).ignoreIfNull().isOneOfTheseValues(
|
baseDataValidator.reset().parameter(paramNameForFinancialActivity).value(financialActivityId).ignoreIfNull()
|
||||||
FinancialActivity.ASSET_TRANSFER.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(),
|
.isOneOfTheseValues(MAPPABLE_FINANCIAL_ACTIVITIES);
|
||||||
FinancialActivity.OPENING_BALANCES_TRANSFER_CONTRA.getValue(), FinancialActivity.ASSET_FUND_SOURCE.getValue(),
|
|
||||||
FinancialActivity.PAYABLE_DIVIDENDS.getValue());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(paramNameForGLAccount, element)) {
|
if (this.fromApiJsonHelper.parameterExists(paramNameForGLAccount, element)) {
|
||||||
|
|||||||
-1
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.accounting.journalentry;
|
package org.apache.fineract.accounting.journalentry;
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||||
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
|
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
|
||||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
||||||
|
|||||||
+5
@@ -37,6 +37,7 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce
|
|||||||
DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, //
|
DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, //
|
||||||
GL_ACCOUNT_DISABLED, //
|
GL_ACCOUNT_DISABLED, //
|
||||||
GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED, //
|
GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED, //
|
||||||
|
GL_ACCOUNT_IS_HEADER, //
|
||||||
INVALID_DEBIT_OR_CREDIT_ACCOUNTS; //
|
INVALID_DEBIT_OR_CREDIT_ACCOUNTS; //
|
||||||
|
|
||||||
public String errorMessage() {
|
public String errorMessage() {
|
||||||
@@ -58,6 +59,8 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce
|
|||||||
return "Invalid debit or credit accounts are passed";
|
return "Invalid debit or credit accounts are passed";
|
||||||
} else if (name().equalsIgnoreCase("GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED")) {
|
} else if (name().equalsIgnoreCase("GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED")) {
|
||||||
return "Target account does not allow manual adjustments";
|
return "Target account does not allow manual adjustments";
|
||||||
|
} else if (name().equalsIgnoreCase("GL_ACCOUNT_IS_HEADER")) {
|
||||||
|
return "Target account is a Header account (a chart-of-accounts grouping node) and cannot receive a transaction directly";
|
||||||
}
|
}
|
||||||
return name();
|
return name();
|
||||||
}
|
}
|
||||||
@@ -79,6 +82,8 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce
|
|||||||
return "error.msg.glJournalEntry.invalid.debit.or.credit.accounts";
|
return "error.msg.glJournalEntry.invalid.debit.or.credit.accounts";
|
||||||
} else if (name().equalsIgnoreCase("GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED")) {
|
} else if (name().equalsIgnoreCase("GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED")) {
|
||||||
return "error.msg.glJournalEntry.invalid.account.manual.adjustments.not.permitted";
|
return "error.msg.glJournalEntry.invalid.account.manual.adjustments.not.permitted";
|
||||||
|
} else if (name().equalsIgnoreCase("GL_ACCOUNT_IS_HEADER")) {
|
||||||
|
return "error.msg.glJournalEntry.invalid.account.is.header";
|
||||||
}
|
}
|
||||||
return name();
|
return name();
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-16
@@ -181,10 +181,14 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
|
|||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue()).value(savingsReferenceAccountId)
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue()).value(savingsReferenceAccountId)
|
||||||
.notNull().integerGreaterThanZero();
|
.notNull().integerGreaterThanZero();
|
||||||
|
|
||||||
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
|
// Transfers In Suspense is not offered by every savings product UI, so it is validated only
|
||||||
.extractLongNamed(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
|
// when the client actually sends it. Omitting it leaves the mapping unset rather than failing.
|
||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
|
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element)) {
|
||||||
.value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero();
|
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
|
||||||
|
.extractLongNamed(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
|
||||||
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
|
||||||
|
.value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero();
|
||||||
|
}
|
||||||
|
|
||||||
final Long interestOnSavingsAccountId = this.fromApiJsonHelper
|
final Long interestOnSavingsAccountId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), element);
|
.extractLongNamed(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), element);
|
||||||
@@ -210,20 +214,29 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) {
|
if (!accountType.equals(DepositAccountType.RECURRING_DEPOSIT) && !accountType.equals(DepositAccountType.FIXED_DEPOSIT)) {
|
||||||
final Long overdraftAccount = this.fromApiJsonHelper
|
// Overdraft Control / Income From Interest / Write Off apply to overdraft-capable products
|
||||||
.extractLongNamed(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), element);
|
// only, so they are validated when supplied rather than demanded from every savings product.
|
||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue())
|
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(),
|
||||||
.value(overdraftAccount).notNull().integerGreaterThanZero();
|
element)) {
|
||||||
|
final Long overdraftAccount = this.fromApiJsonHelper
|
||||||
|
.extractLongNamed(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), element);
|
||||||
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue())
|
||||||
|
.value(overdraftAccount).notNull().integerGreaterThanZero();
|
||||||
|
}
|
||||||
|
|
||||||
final Long incomeFromInterest = this.fromApiJsonHelper
|
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element)) {
|
||||||
.extractLongNamed(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element);
|
final Long incomeFromInterest = this.fromApiJsonHelper
|
||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue()).value(incomeFromInterest)
|
.extractLongNamed(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element);
|
||||||
.notNull().integerGreaterThanZero();
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue())
|
||||||
|
.value(incomeFromInterest).notNull().integerGreaterThanZero();
|
||||||
|
}
|
||||||
|
|
||||||
final Long writtenOff = this.fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(),
|
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element)) {
|
||||||
element);
|
final Long writtenOff = this.fromApiJsonHelper
|
||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenOff).notNull()
|
.extractLongNamed(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element);
|
||||||
.integerGreaterThanZero();
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenOff)
|
||||||
|
.notNull().integerGreaterThanZero();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -277,14 +277,14 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element);
|
final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element);
|
||||||
/**
|
/*
|
||||||
* Variable stores a map representation of charges (key) and their associated income Id's (value) extracted from
|
* Variable stores a map representation of charges (key) and their associated income Id's (value) extracted from
|
||||||
* the passed in Jsoncommand
|
* the passed in Jsoncommand
|
||||||
**/
|
*/
|
||||||
final Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<>();
|
final Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<>();
|
||||||
/***
|
/*
|
||||||
* Variable stores all charges which have already been mapped to Income Accounts in the system
|
* Variable stores all charges which have already been mapped to Income Accounts in the system
|
||||||
**/
|
*/
|
||||||
final Set<Long> existingCharges = new HashSet<>();
|
final Set<Long> existingCharges = new HashSet<>();
|
||||||
if (chargeToIncomeAccountMappingArray != null) {
|
if (chargeToIncomeAccountMappingArray != null) {
|
||||||
if (changes != null) {
|
if (changes != null) {
|
||||||
@@ -301,13 +301,13 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
// If input map is empty, delete all existing mappings
|
// If input map is empty, delete all existing mappings
|
||||||
if (inputChargeToIncomeAccountMap.size() == 0) {
|
if (inputChargeToIncomeAccountMap.size() == 0) {
|
||||||
this.accountMappingRepository.deleteAll(existingChargeToIncomeAccountMappings);
|
this.accountMappingRepository.deleteAll(existingChargeToIncomeAccountMappings);
|
||||||
} /**
|
} /*
|
||||||
* Else, <br/>
|
* Else, <br/>
|
||||||
* update existing mappings OR <br/>
|
* update existing mappings OR <br/>
|
||||||
* delete old mappings (which are already present, but not passed in as a part of Jsoncommand)<br/>
|
* delete old mappings (which are already present, but not passed in as a part of Jsoncommand)<br/>
|
||||||
* Create new mappings for charges that are passed in as a part of the Jsoncommand but not already present
|
* Create new mappings for charges that are passed in as a part of the Jsoncommand but not already present
|
||||||
*
|
*
|
||||||
**/
|
*/
|
||||||
else {
|
else {
|
||||||
for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) {
|
for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) {
|
||||||
final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId();
|
final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId();
|
||||||
@@ -356,14 +356,14 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
.findAllPaymentTypeToFundSourceMappings(productId, portfolioProductType.getValue());
|
.findAllPaymentTypeToFundSourceMappings(productId, portfolioProductType.getValue());
|
||||||
final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper
|
final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper
|
||||||
.extractJsonArrayNamed(LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element);
|
.extractJsonArrayNamed(LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element);
|
||||||
/**
|
/*
|
||||||
* Variable stores a map representation of Payment channels (key) and their fund sources (value) extracted from
|
* Variable stores a map representation of Payment channels (key) and their fund sources (value) extracted from
|
||||||
* the passed in Jsoncommand
|
* the passed in Jsoncommand
|
||||||
**/
|
*/
|
||||||
final Map<Long, Long> inputPaymentChannelFundSourceMap = new HashMap<>();
|
final Map<Long, Long> inputPaymentChannelFundSourceMap = new HashMap<>();
|
||||||
/***
|
/*
|
||||||
* Variable stores all payment types which have already been mapped to Fund Sources in the system
|
* Variable stores all payment types which have already been mapped to Fund Sources in the system
|
||||||
**/
|
*/
|
||||||
final Set<Long> existingPaymentTypes = new HashSet<>();
|
final Set<Long> existingPaymentTypes = new HashSet<>();
|
||||||
if (paymentChannelMappingArray != null) {
|
if (paymentChannelMappingArray != null) {
|
||||||
if (changes != null) {
|
if (changes != null) {
|
||||||
@@ -381,14 +381,14 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
// If input map is empty, delete all existing mappings
|
// If input map is empty, delete all existing mappings
|
||||||
if (inputPaymentChannelFundSourceMap.isEmpty()) {
|
if (inputPaymentChannelFundSourceMap.isEmpty()) {
|
||||||
this.accountMappingRepository.deleteAll(existingPaymentChannelToFundSourceMappings);
|
this.accountMappingRepository.deleteAll(existingPaymentChannelToFundSourceMappings);
|
||||||
} /**
|
} /*
|
||||||
* Else, <br/>
|
* Else, <br/>
|
||||||
* update existing mappings OR <br/>
|
* update existing mappings OR <br/>
|
||||||
* delete old mappings (which re already present, but not passed in as a part of Jsoncommand)<br/>
|
* delete old mappings (which re already present, but not passed in as a part of Jsoncommand)<br/>
|
||||||
* Create new mappings for payment types that are passed in as a part of the Jsoncommand but not already
|
* Create new mappings for payment types that are passed in as a part of the Jsoncommand but not already
|
||||||
* present
|
* present
|
||||||
*
|
*
|
||||||
**/
|
*/
|
||||||
else {
|
else {
|
||||||
for (final ProductToGLAccountMapping existingPaymentChannelToFundSourceMapping : existingPaymentChannelToFundSourceMappings) {
|
for (final ProductToGLAccountMapping existingPaymentChannelToFundSourceMapping : existingPaymentChannelToFundSourceMappings) {
|
||||||
final Long currentPaymentChannelId = existingPaymentChannelToFundSourceMapping.getPaymentType().getId();
|
final Long currentPaymentChannelId = existingPaymentChannelToFundSourceMapping.getPaymentType().getId();
|
||||||
@@ -584,9 +584,9 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
// based on input condition
|
// based on input condition
|
||||||
|
|
||||||
GLAccount glAccount;
|
GLAccount glAccount;
|
||||||
/**
|
/*
|
||||||
* Both CASH and Accrual placeholders have the same value for income from Interest and penalties
|
* Both CASH and Accrual placeholders have the same value for income from Interest and penalties
|
||||||
**/
|
*/
|
||||||
CashAccountsForLoan placeHolderAccountType;
|
CashAccountsForLoan placeHolderAccountType;
|
||||||
if (isPenalty) {
|
if (isPenalty) {
|
||||||
glAccount = getAccountByIdAndType(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), GLAccountType.INCOME,
|
glAccount = getAccountByIdAndType(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), GLAccountType.INCOME,
|
||||||
|
|||||||
+4
@@ -426,6 +426,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
|||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData);
|
||||||
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.ESCHEAT_LIABILITY)) {
|
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.ESCHEAT_LIABILITY)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData);
|
||||||
|
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.ROUNDING_ADJUSTMENT)) {
|
||||||
|
accountMappingDetails.put(SavingProductAccountingDataParams.ROUNDING_ADJUSTMENT.getValue(), glAccountData);
|
||||||
// Expense
|
// Expense
|
||||||
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_ON_SAVINGS)) {
|
} else if (glAccountForSavings.equals(AccrualAccountsForSavings.INTEREST_ON_SAVINGS)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData);
|
||||||
@@ -470,6 +472,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
|||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INCOME_FROM_INTEREST.getValue(), glAccountData);
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.ESCHEAT_LIABILITY)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.ESCHEAT_LIABILITY)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.ESCHEAT_LIABILITY.getValue(), glAccountData);
|
||||||
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.ROUNDING_ADJUSTMENT)) {
|
||||||
|
accountMappingDetails.put(SavingProductAccountingDataParams.ROUNDING_ADJUSTMENT.getValue(), glAccountData);
|
||||||
// Expense
|
// Expense
|
||||||
} else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_ON_SAVINGS)) {
|
} else if (glAccountForSavings.equals(CashAccountsForSavings.INTEREST_ON_SAVINGS)) {
|
||||||
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData);
|
accountMappingDetails.put(SavingProductAccountingDataParams.INTEREST_ON_SAVINGS.getValue(), glAccountData);
|
||||||
|
|||||||
+18
@@ -106,6 +106,20 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
|||||||
PortfolioProductType.SAVING);
|
PortfolioProductType.SAVING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create-or-merge, for an expense row a product may not have yet.
|
||||||
|
*
|
||||||
|
* The plain merge variant assumes the mapping already exists, which is true of every expense
|
||||||
|
* row that shipped with the product screen. A row added later — the rounding adjustment — is
|
||||||
|
* absent on every product created before it, so editing one of those has to create the
|
||||||
|
* mapping rather than fail looking for it.
|
||||||
|
*/
|
||||||
|
public void createOrmergeSavingsToExpenseAccountMappingChanges(final JsonElement element, final String paramName,
|
||||||
|
final Long productId, final int accountTypeId, final Map<String, Object> changes) {
|
||||||
|
createOrmergeProductToAccountMappingChanges(element, paramName, productId, accountTypeId, changes, GLAccountType.EXPENSE,
|
||||||
|
PortfolioProductType.SAVING);
|
||||||
|
}
|
||||||
|
|
||||||
/*** Abstractions for payments channel related to savings products ***/
|
/*** Abstractions for payments channel related to savings products ***/
|
||||||
|
|
||||||
public void savePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
public void savePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
||||||
@@ -248,6 +262,8 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
|||||||
CashAccountsForSavings.TRANSFERS_SUSPENSE.toString(), changes);
|
CashAccountsForSavings.TRANSFERS_SUSPENSE.toString(), changes);
|
||||||
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(),
|
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(),
|
||||||
savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
savingsProductId, CashAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
||||||
|
createOrmergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
|
||||||
|
savingsProductId, CashAccountsForSavings.ROUNDING_ADJUSTMENT.getValue(), changes);
|
||||||
break;
|
break;
|
||||||
case ACCRUAL_PERIODIC:
|
case ACCRUAL_PERIODIC:
|
||||||
// asset
|
// asset
|
||||||
@@ -308,6 +324,8 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
|||||||
|
|
||||||
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(),
|
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(),
|
||||||
savingsProductId, AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
savingsProductId, AccrualAccountsForSavings.ESCHEAT_LIABILITY.getValue(), changes);
|
||||||
|
createOrmergeSavingsToExpenseAccountMappingChanges(element, SavingProductAccountingParams.ROUNDING_ADJUSTMENT.getValue(),
|
||||||
|
savingsProductId, AccrualAccountsForSavings.ROUNDING_ADJUSTMENT.getValue(), changes);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ACCRUAL_UPFRONT:
|
case ACCRUAL_UPFRONT:
|
||||||
|
|||||||
+1
@@ -54,6 +54,7 @@
|
|||||||
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
||||||
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
||||||
|
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
|
||||||
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
||||||
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
||||||
|
|||||||
+2
-2
@@ -181,12 +181,12 @@ public class TellerApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Operation(summary = "Create Cashiers", description = """
|
@Operation(summary = "Create Cashiers", description = """
|
||||||
Mandatory Fields:\s
|
Mandatory Fields:
|
||||||
Cashier/staff, Fromm Date, To Date, Full Day or From time and To time
|
Cashier/staff, Fromm Date, To Date, Full Day or From time and To time
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Optional Fields:\s
|
Optional Fields:
|
||||||
Description/Notes""")
|
Description/Notes""")
|
||||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersRequest.class)))
|
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersRequest.class)))
|
||||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersResponse.class)))
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersResponse.class)))
|
||||||
|
|||||||
+1
@@ -54,6 +54,7 @@
|
|||||||
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
||||||
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
||||||
|
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
|
||||||
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
||||||
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
||||||
|
|||||||
+1
@@ -54,6 +54,7 @@
|
|||||||
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
||||||
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
||||||
|
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
|
||||||
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
||||||
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ task cleanupGeneratedJavaFiles() {
|
|||||||
// Configure Java compilation
|
// Configure Java compilation
|
||||||
tasks.named('compileJava') {
|
tasks.named('compileJava') {
|
||||||
outputs.cacheIf { true }
|
outputs.cacheIf { true }
|
||||||
dependsOn(buildJavaSdk, buildTypescriptAngularSdk, buildAsciidoc, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply)
|
dependsOn(buildJavaSdk, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply)
|
||||||
mustRunAfter(licenseFormatMain, cleanupGeneratedJavaFiles)
|
mustRunAfter(licenseFormatMain, cleanupGeneratedJavaFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
||||||
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
||||||
|
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
|
||||||
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
||||||
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
||||||
|
|||||||
+45
-5
@@ -269,7 +269,15 @@ public final class AccountingConstants {
|
|||||||
OVERDRAFT_PORTFOLIO_CONTROL(11), //
|
OVERDRAFT_PORTFOLIO_CONTROL(11), //
|
||||||
INCOME_FROM_INTEREST(12), //
|
INCOME_FROM_INTEREST(12), //
|
||||||
LOSSES_WRITTEN_OFF(13), //
|
LOSSES_WRITTEN_OFF(13), //
|
||||||
ESCHEAT_LIABILITY(14); //
|
ESCHEAT_LIABILITY(14), //
|
||||||
|
/**
|
||||||
|
* Where the paise dropped from a cash closure are absorbed.
|
||||||
|
*
|
||||||
|
* Cash has no paise in circulation, so a closing balance of 9,883.79 is handed over as
|
||||||
|
* 9,883 and the 0.79 has to land somewhere other than the member's own ledger. Numbered
|
||||||
|
* well clear of the accrual slots so the two sets never collide as either grows.
|
||||||
|
*/
|
||||||
|
ROUNDING_ADJUSTMENT(26); //
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
|
|
||||||
@@ -317,7 +325,9 @@ public final class AccountingConstants {
|
|||||||
FEES_RECEIVABLE(15), //
|
FEES_RECEIVABLE(15), //
|
||||||
PENALTIES_RECEIVABLE(16), //
|
PENALTIES_RECEIVABLE(16), //
|
||||||
INTEREST_PAYABLE(17), //
|
INTEREST_PAYABLE(17), //
|
||||||
INTEREST_RECEIVABLE(18);
|
INTEREST_RECEIVABLE(18), //
|
||||||
|
/** Where the paise dropped from a cash closure are absorbed — see the cash enum. */
|
||||||
|
ROUNDING_ADJUSTMENT(26);
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
|
|
||||||
@@ -372,7 +382,8 @@ public final class AccountingConstants {
|
|||||||
PENALTIES_RECEIVABLE("penaltiesReceivableAccountId"), //
|
PENALTIES_RECEIVABLE("penaltiesReceivableAccountId"), //
|
||||||
INTEREST_RECEIVABLE("interestReceivableAccountId"), //
|
INTEREST_RECEIVABLE("interestReceivableAccountId"), //
|
||||||
FEES_RECEIVABLE("feesReceivableAccountId"), //
|
FEES_RECEIVABLE("feesReceivableAccountId"), //
|
||||||
INTEREST_PAYABLE("interestPayableAccountId");
|
INTEREST_PAYABLE("interestPayableAccountId"), //
|
||||||
|
ROUNDING_ADJUSTMENT("roundingAdjustmentAccountId");
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
@@ -410,7 +421,8 @@ public final class AccountingConstants {
|
|||||||
FEES_RECEIVABLE("feeReceivableAccount"), //
|
FEES_RECEIVABLE("feeReceivableAccount"), //
|
||||||
PENALTIES_RECEIVABLE("penaltyReceivableAccount"), //
|
PENALTIES_RECEIVABLE("penaltyReceivableAccount"), //
|
||||||
INTEREST_PAYABLE("interestPayableAccount"), //
|
INTEREST_PAYABLE("interestPayableAccount"), //
|
||||||
INTEREST_RECEIVABLE("interestReceivableAccount"); //
|
INTEREST_RECEIVABLE("interestReceivableAccount"), //
|
||||||
|
ROUNDING_ADJUSTMENT("roundingAdjustmentAccount"); //
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
@@ -436,7 +448,35 @@ public final class AccountingConstants {
|
|||||||
CASH_AT_TELLER(102, "cashAtTeller", GLAccountType.ASSET), //
|
CASH_AT_TELLER(102, "cashAtTeller", GLAccountType.ASSET), //
|
||||||
OPENING_BALANCES_TRANSFER_CONTRA(300, "openingBalancesTransferContra", GLAccountType.EQUITY), //
|
OPENING_BALANCES_TRANSFER_CONTRA(300, "openingBalancesTransferContra", GLAccountType.EQUITY), //
|
||||||
ASSET_FUND_SOURCE(103, "fundSource", GLAccountType.ASSET), //
|
ASSET_FUND_SOURCE(103, "fundSource", GLAccountType.ASSET), //
|
||||||
PAYABLE_DIVIDENDS(201, "payableDividends", GLAccountType.LIABILITY); //
|
PAYABLE_DIVIDENDS(201, "payableDividends", GLAccountType.LIABILITY), //
|
||||||
|
DIVIDEND_PAID(202, "dividendPaid", GLAccountType.LIABILITY), //
|
||||||
|
RESERVE_FOR_DIVIDEND(301, "reserveForDividend", GLAccountType.EQUITY), //
|
||||||
|
DIVIDEND_FORFEITED_INCOME(400, "dividendForfeitedIncome", GLAccountType.INCOME), //
|
||||||
|
/**
|
||||||
|
* The year's closed-out profit a dividend is declared against.
|
||||||
|
*
|
||||||
|
* Kept apart from {@link #RETAINED_PROFIT} on purpose: a declaration is sized on this balance, so anything that
|
||||||
|
* has already been through a distribution decision must not sit here, or the same money would be offered for
|
||||||
|
* distribution a second time.
|
||||||
|
*/
|
||||||
|
DISTRIBUTABLE_PROFIT(302, "distributableProfit", GLAccountType.EQUITY), //
|
||||||
|
/**
|
||||||
|
* Where profit settles once it is no longer distributable - the remainder left after an approved declaration
|
||||||
|
* and dividends forfeited after the three-year claim window. Nothing here ever enters a later dividend
|
||||||
|
* calculation.
|
||||||
|
*/
|
||||||
|
RETAINED_PROFIT(303, "retainedProfit", GLAccountType.EQUITY), //
|
||||||
|
/**
|
||||||
|
* Where the paise a dividend cannot divide evenly are collected.
|
||||||
|
*
|
||||||
|
* A declaration of 1,538.91 that allocates 1,538 to members leaves 0.91 that belongs to nobody. It used to be
|
||||||
|
* swept into retained profit along with the undistributed remainder, which buried an arithmetic artefact inside
|
||||||
|
* a figure the society reads as a real business result. Given its own account it stays separately readable -
|
||||||
|
* and an auditor asking why members received less than was declared can be answered from the ledger.
|
||||||
|
*
|
||||||
|
* Optional: a society that has not mapped it has the residue swept to retained profit exactly as before.
|
||||||
|
*/
|
||||||
|
DIVIDEND_ROUND_OFF(304, "dividendRoundOff", GLAccountType.EQUITY); //
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
|
|||||||
+20
-1
@@ -137,7 +137,7 @@ public class CommandSourceService {
|
|||||||
String permission = commandSource.getPermissionCode();
|
String permission = commandSource.getPermissionCode();
|
||||||
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
|
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
|
||||||
if (isMakerChecker || result.isRollbackTransaction()) {
|
if (isMakerChecker || result.isRollbackTransaction()) {
|
||||||
if (isApprovedByChecker || user.isCheckerSuperUser()) {
|
if (isApprovedByChecker || isSuperUserBypassAllowed(commandSource, user)) {
|
||||||
commandSource.markAsChecked(user);
|
commandSource.markAsChecked(user);
|
||||||
} else {
|
} else {
|
||||||
if (commandSource.isSanitized()) {
|
if (commandSource.isSanitized()) {
|
||||||
@@ -151,6 +151,25 @@ public class CommandSourceService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSuperUserBypassAllowed(CommandSource commandSource, AppUser user) {
|
||||||
|
if (!user.isCheckerSuperUser()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CHARGE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) {
|
private void sanitizeJson(@NonNull CommandSource commandSource, Set<String> sanitizeKeys) {
|
||||||
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
|
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+41
@@ -640,6 +640,14 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createUserAgreement() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "USERAGREEMENT";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/useragreements";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createOffice() {
|
public CommandWrapperBuilder createOffice() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "OFFICE";
|
this.entityName = "OFFICE";
|
||||||
@@ -656,6 +664,14 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteOffice(final Long officeId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "OFFICE";
|
||||||
|
this.entityId = officeId;
|
||||||
|
this.href = "/offices/" + officeId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createOfficeTransaction() {
|
public CommandWrapperBuilder createOfficeTransaction() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "OFFICETRANSACTION";
|
this.entityName = "OFFICETRANSACTION";
|
||||||
@@ -2203,6 +2219,14 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createCbsBaseInterestRate() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "CBSBASEINTERESTRATE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/fixeddepositproducts/baseinterestrates";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) {
|
public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) {
|
||||||
this.actionName = "UPDATE";
|
this.actionName = "UPDATE";
|
||||||
this.entityName = "FIXEDDEPOSITPRODUCT";
|
this.entityName = "FIXEDDEPOSITPRODUCT";
|
||||||
@@ -2889,6 +2913,15 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder paymentFixedDepositAccount(final Long accountId) {
|
||||||
|
this.actionName = "PAYMENT";
|
||||||
|
this.entityName = "FIXEDDEPOSITACCOUNT";
|
||||||
|
this.entityId = accountId;
|
||||||
|
this.savingsId = accountId;
|
||||||
|
this.href = "/fixeddepositaccounts/" + accountId + "?command=payment";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder fixedDepositAccountInterestCalculation(final Long accountId) {
|
public CommandWrapperBuilder fixedDepositAccountInterestCalculation(final Long accountId) {
|
||||||
this.actionName = "CALCULATEINTEREST";
|
this.actionName = "CALCULATEINTEREST";
|
||||||
this.entityName = "FIXEDDEPOSITACCOUNT";
|
this.entityName = "FIXEDDEPOSITACCOUNT";
|
||||||
@@ -3095,6 +3128,14 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateFacilityChargeMapping(final Long mappingId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "FACILITYCHARGEMAPPING";
|
||||||
|
this.entityId = mappingId;
|
||||||
|
this.href = "/facilitychargemappings/" + mappingId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteOfficeToGLAccountMapping(final Long mappingId) {
|
public CommandWrapperBuilder deleteOfficeToGLAccountMapping(final Long mappingId) {
|
||||||
this.actionName = "DELETE";
|
this.actionName = "DELETE";
|
||||||
this.entityName = "FINANCIALACTIVITYACCOUNT";
|
this.entityName = "FINANCIALACTIVITYACCOUNT";
|
||||||
|
|||||||
+2
@@ -26,6 +26,8 @@ public interface ConfigurationDomainService {
|
|||||||
|
|
||||||
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
|
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
|
||||||
|
|
||||||
|
boolean isMakerCheckerEnabled();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least
|
* Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least
|
||||||
* one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge
|
* one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge
|
||||||
|
|||||||
+13
@@ -130,6 +130,19 @@ public class DataValidatorBuilder {
|
|||||||
this.dataValidationErrors.add(error);
|
this.dataValidationErrors.add(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link #failWithCode(String, Object...)}, but the caller's own message is what's actually shown to the
|
||||||
|
* user (that method always renders the generic "Failed data validation due to: {errorCode}." regardless of the
|
||||||
|
* args passed to it).
|
||||||
|
*/
|
||||||
|
public void failWithCodeAndMessage(final String errorCode, final String defaultUserMessage,
|
||||||
|
final Object... defaultUserMessageArgs) {
|
||||||
|
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + "." + errorCode;
|
||||||
|
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultUserMessage, this.parameter,
|
||||||
|
this.value, defaultUserMessageArgs);
|
||||||
|
this.dataValidationErrors.add(error);
|
||||||
|
}
|
||||||
|
|
||||||
public void failWithCodeNoParameterAddedToErrorCode(final String errorCode, final Object... defaultUserMessageArgs) {
|
public void failWithCodeNoParameterAddedToErrorCode(final String errorCode, final Object... defaultUserMessageArgs) {
|
||||||
String validationErrorCode = "validation.msg." + this.resource + "." + errorCode;
|
String validationErrorCode = "validation.msg." + this.resource + "." + errorCode;
|
||||||
String defaultEnglishMessage = "Failed data validation due to: " + errorCode + ".";
|
String defaultEnglishMessage = "Failed data validation due to: " + errorCode + ".";
|
||||||
|
|||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.documentmanagement.adapter;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decouples the entity "current signature" pointer from the generic document service, the same
|
||||||
|
* way {@link EntityImageIdAdapter} decouples the "current photo" pointer — lets
|
||||||
|
* DocumentWritePlatformServiceImpl (fineract-document module) give single-slot upsert semantics to
|
||||||
|
* signature uploads without depending on portfolio-module entities directly.
|
||||||
|
*/
|
||||||
|
public interface EntitySignatureIdAdapter {
|
||||||
|
|
||||||
|
/** @param documentName the m_document.name of the document being created/removed */
|
||||||
|
boolean accept(String entityType, String documentName);
|
||||||
|
|
||||||
|
Optional<SignatureIdResult> get(Long entityId);
|
||||||
|
|
||||||
|
Optional<SignatureIdResult> set(Long entityId, Long signatureId);
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
class SignatureIdResult implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -38,7 +38,11 @@ public enum JobName {
|
|||||||
GENERATE_RD_SCEHDULE("Generate Mandatory Savings Schedule"), //
|
GENERATE_RD_SCEHDULE("Generate Mandatory Savings Schedule"), //
|
||||||
GENERATE_LOANLOSS_PROVISIONING("Generate Loan Loss Provisioning"), //
|
GENERATE_LOANLOSS_PROVISIONING("Generate Loan Loss Provisioning"), //
|
||||||
POST_DIVIDENTS_FOR_SHARES("Post Dividends For Shares"), //
|
POST_DIVIDENTS_FOR_SHARES("Post Dividends For Shares"), //
|
||||||
|
FORFEIT_UNCLAIMED_DIVIDENDS("Forfeit Unclaimed Dividends"), //
|
||||||
|
POST_SHARE_ENTRY_FEE_TO_PROFIT("Post Share Entry Fee To Profit"), //
|
||||||
UPDATE_SAVINGS_DORMANT_ACCOUNTS("Update Savings Dormant Accounts"), //
|
UPDATE_SAVINGS_DORMANT_ACCOUNTS("Update Savings Dormant Accounts"), //
|
||||||
|
APPLY_KYC_EXPIRY_RESTRICTIONS("Apply KYC Expiry Restrictions"), //
|
||||||
|
SAVINGS_DAILY_INTEREST("Savings Daily Interest Accrual And Posting"), //
|
||||||
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS(
|
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS(
|
||||||
"Add Accrual Transactions For Loans With Income Posted As Transactions"), //
|
"Add Accrual Transactions For Loans With Income Posted As Transactions"), //
|
||||||
EXECUTE_REPORT_MAILING_JOBS("Execute Report Mailing Jobs"), //
|
EXECUTE_REPORT_MAILING_JOBS("Execute Report Mailing Jobs"), //
|
||||||
@@ -52,6 +56,7 @@ public enum JobName {
|
|||||||
EXECUTE_DIRTY_JOBS("Execute All Dirty Jobs"), //
|
EXECUTE_DIRTY_JOBS("Execute All Dirty Jobs"), //
|
||||||
INCREASE_BUSINESS_DATE_BY_1_DAY("Increase Business Date by 1 day"), //
|
INCREASE_BUSINESS_DATE_BY_1_DAY("Increase Business Date by 1 day"), //
|
||||||
INCREASE_COB_DATE_BY_1_DAY("Increase COB Date by 1 day"), //
|
INCREASE_COB_DATE_BY_1_DAY("Increase COB Date by 1 day"), //
|
||||||
|
SYNC_BUSINESS_DATE_TO_SYSTEM_DATE("Sync Business Date To System Date"), //
|
||||||
LOAN_COB("Loan COB"), //
|
LOAN_COB("Loan COB"), //
|
||||||
LOAN_DELINQUENCY_CLASSIFICATION("Loan Delinquency Classification"), //
|
LOAN_DELINQUENCY_CLASSIFICATION("Loan Delinquency Classification"), //
|
||||||
SEND_ASYNCHRONOUS_EVENTS("Send Asynchronous Events"), //
|
SEND_ASYNCHRONOUS_EVENTS("Send Asynchronous Events"), //
|
||||||
@@ -65,6 +70,8 @@ public enum JobName {
|
|||||||
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
|
NEFT_BATCH_PROCESS("NEFT Batch Process"), //
|
||||||
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
|
VERIFICATION_TOKEN_CLEANUP("Verification Token Cleanup"), //
|
||||||
OUTWARD_CLEARING_BATCH_SEND("CTS Outward Clearing Batch Send"), //
|
OUTWARD_CLEARING_BATCH_SEND("CTS Outward Clearing Batch Send"), //
|
||||||
|
CTR_REPORT_GENERATION("AML Monthly CTR Report Generation"), //
|
||||||
|
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
|
||||||
; //
|
; //
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|||||||
+11
@@ -61,4 +61,15 @@ public class OfficeRepositoryWrapper {
|
|||||||
public void delete(final Office entity) {
|
public void delete(final Office entity) {
|
||||||
this.repository.delete(entity);
|
this.repository.delete(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes and flushes in the same call, so a foreign-key violation the caller did not already check
|
||||||
|
* for (a savings transaction, a GL journal entry, a teller position -- anything on m_office's long
|
||||||
|
* tail of referencing tables) surfaces synchronously here, inside the caller's own try/catch, rather
|
||||||
|
* than at transaction-commit time where it can no longer be turned into a clean error message.
|
||||||
|
*/
|
||||||
|
public void deleteAndFlush(final Office entity) {
|
||||||
|
this.repository.delete(entity);
|
||||||
|
this.repository.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -72,11 +72,26 @@ public class SavingsAccountSummaryData {
|
|||||||
// differentiate deposit accounts Savings, FD and RD accounts
|
// differentiate deposit accounts Savings, FD and RD accounts
|
||||||
private final EnumOptionData depositType;
|
private final EnumOptionData depositType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a Fixed Deposit, the stable customer-facing FD number: the account number of the first account in the renewal
|
||||||
|
* chain, which stays the same across every renewal even though each term is its own account row. Equals accountNo
|
||||||
|
* for an FD that has never been renewed, and is null for non-deposit accounts.
|
||||||
|
*/
|
||||||
|
private final String fdAccountNumber;
|
||||||
|
/**
|
||||||
|
* Which renewal cycle of that FD this row is: 0 for the originally opened account, 1 for the first renewal, and so
|
||||||
|
* on. Lets a client's account list collapse a renewal chain into one FD instead of listing each term separately.
|
||||||
|
*/
|
||||||
|
private final Integer renewalCycleNo;
|
||||||
|
|
||||||
public SavingsAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId,
|
public SavingsAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId,
|
||||||
final String productName, final String shortProductName, final SavingsAccountStatusEnumData status, final CurrencyData currency,
|
final String productName, final String shortProductName, final SavingsAccountStatusEnumData status, final CurrencyData currency,
|
||||||
final BigDecimal accountBalance, final BigDecimal onHoldFunds, final BigDecimal savingsAmountOnHold,
|
final BigDecimal accountBalance, final BigDecimal onHoldFunds, final BigDecimal savingsAmountOnHold,
|
||||||
final BigDecimal availableBalance, final EnumOptionData accountType, final SavingsAccountApplicationTimelineData timeline,
|
final BigDecimal availableBalance, final EnumOptionData accountType, final SavingsAccountApplicationTimelineData timeline,
|
||||||
final EnumOptionData depositType, final SavingsAccountSubStatusEnumData subStatus, final LocalDate lastActiveTransactionDate) {
|
final EnumOptionData depositType, final SavingsAccountSubStatusEnumData subStatus, final LocalDate lastActiveTransactionDate,
|
||||||
|
final String fdAccountNumber, final Integer renewalCycleNo) {
|
||||||
|
this.fdAccountNumber = fdAccountNumber;
|
||||||
|
this.renewalCycleNo = renewalCycleNo;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.accountNo = accountNo;
|
this.accountNo = accountNo;
|
||||||
this.externalId = externalId;
|
this.externalId = externalId;
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row per {@link AmlTransactionRule} hit on a live transaction. Raised regardless of the
|
||||||
|
* client's current KYC risk category - alerting is never suppressed by a client being Low/Medium
|
||||||
|
* risk. First reviewed by an AML Officer (whitelist or escalate); an ESCALATED alert spawns an
|
||||||
|
* {@link AmlStrCase} for the Principal Officer.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_aml_alert")
|
||||||
|
public class AmlAlert extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "transaction_type", nullable = false, length = 20)
|
||||||
|
private String transactionType;
|
||||||
|
|
||||||
|
@Column(name = "transaction_id", nullable = false)
|
||||||
|
private Long transactionId;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "rule_id", nullable = false)
|
||||||
|
private AmlTransactionRule rule;
|
||||||
|
|
||||||
|
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@Column(name = "currency_code", length = 3)
|
||||||
|
private String currencyCode;
|
||||||
|
|
||||||
|
@Column(name = "raised_on", nullable = false)
|
||||||
|
private LocalDateTime raisedOn;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "reviewed_by")
|
||||||
|
private AppUser reviewedBy;
|
||||||
|
|
||||||
|
@Column(name = "reviewed_on")
|
||||||
|
private LocalDateTime reviewedOn;
|
||||||
|
|
||||||
|
@Column(name = "review_notes", length = 1000)
|
||||||
|
private String reviewNotes;
|
||||||
|
|
||||||
|
@Column(name = "disposition", length = 20)
|
||||||
|
private String disposition;
|
||||||
|
|
||||||
|
protected AmlAlert() {}
|
||||||
|
|
||||||
|
public static AmlAlert raise(AmlTransactionType transactionType, Long transactionId, Client client, AmlTransactionRule rule,
|
||||||
|
BigDecimal amount, String currencyCode, LocalDateTime raisedOn) {
|
||||||
|
final AmlAlert alert = new AmlAlert();
|
||||||
|
alert.transactionType = transactionType.name();
|
||||||
|
alert.transactionId = transactionId;
|
||||||
|
alert.client = client;
|
||||||
|
alert.rule = rule;
|
||||||
|
alert.amount = amount;
|
||||||
|
alert.currencyCode = currencyCode;
|
||||||
|
alert.raisedOn = raisedOn;
|
||||||
|
alert.status = AmlAlertStatus.RAISED.name();
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void whitelist(AppUser reviewer, String notes) {
|
||||||
|
this.reviewedBy = reviewer;
|
||||||
|
this.reviewedOn = LocalDateTime.now();
|
||||||
|
this.reviewNotes = notes;
|
||||||
|
this.disposition = AmlAlertDisposition.WHITELIST.name();
|
||||||
|
this.status = AmlAlertStatus.WHITELISTED.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void escalate(AppUser reviewer, String notes) {
|
||||||
|
this.reviewedBy = reviewer;
|
||||||
|
this.reviewedOn = LocalDateTime.now();
|
||||||
|
this.reviewNotes = notes;
|
||||||
|
this.disposition = AmlAlertDisposition.ESCALATE.name();
|
||||||
|
this.status = AmlAlertStatus.ESCALATED.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTransactionId() {
|
||||||
|
return transactionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlTransactionType getTransactionType() {
|
||||||
|
return AmlTransactionType.valueOf(transactionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlTransactionRule getRule() {
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrencyCode() {
|
||||||
|
return currencyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getRaisedOn() {
|
||||||
|
return raisedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlAlertStatus getStatus() {
|
||||||
|
return AmlAlertStatus.valueOf(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppUser getReviewedBy() {
|
||||||
|
return reviewedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getReviewedOn() {
|
||||||
|
return reviewedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReviewNotes() {
|
||||||
|
return reviewNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlAlertDisposition getDisposition() {
|
||||||
|
return disposition == null ? null : AmlAlertDisposition.valueOf(disposition);
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
/** The AML Officer's first-level disposition of an {@link AmlAlert}. */
|
||||||
|
public enum AmlAlertDisposition {
|
||||||
|
|
||||||
|
WHITELIST, ESCALATE
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
public interface AmlAlertRepository extends JpaRepository<AmlAlert, Long> {
|
||||||
|
|
||||||
|
List<AmlAlert> findByStatusOrderByRaisedOnAsc(String status);
|
||||||
|
|
||||||
|
@Query("SELECT COALESCE(SUM(a.amount), 0) FROM AmlAlert a WHERE a.client.id = :clientId "
|
||||||
|
+ "AND a.transactionType = :transactionType AND a.raisedOn >= :since")
|
||||||
|
BigDecimal sumAmountForClientSince(@Param("clientId") Long clientId, @Param("transactionType") String transactionType,
|
||||||
|
@Param("since") LocalDateTime since);
|
||||||
|
|
||||||
|
long countByClientId(Long clientId);
|
||||||
|
|
||||||
|
/** Alerts for one customer inside the recurring-behaviour observation window. */
|
||||||
|
long countByClient_IdAndRaisedOnGreaterThanEqual(Long clientId, LocalDateTime since);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alerts already raised against one transaction under one rule. Used to stop the same ledger entry being marked
|
||||||
|
* twice, which would put two STR cases in front of the Principal Officer for a single transaction.
|
||||||
|
*/
|
||||||
|
List<AmlAlert> findByRule_IdAndTransactionId(Long ruleId, Long transactionId);
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
public enum AmlAlertStatus {
|
||||||
|
|
||||||
|
RAISED, UNDER_REVIEW, WHITELISTED, ESCALATED
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
/** Which transaction type(s) an {@link AmlTransactionRule} evaluates against. */
|
||||||
|
public enum AmlAppliesTo {
|
||||||
|
|
||||||
|
SAVINGS, LOAN, TRANSFER, ALL;
|
||||||
|
|
||||||
|
public boolean matches(AmlTransactionType transactionType) {
|
||||||
|
return this == ALL || this.name().equals(transactionType.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monthly, mechanical CTR batch header - purely threshold-based (₹10L+ cash, individually or
|
||||||
|
* connected), no Principal Officer judgment involved. One row per calendar month.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_aml_ctr_report")
|
||||||
|
public class AmlCtrReport extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "report_month", nullable = false, length = 7)
|
||||||
|
private String reportMonth;
|
||||||
|
|
||||||
|
@Column(name = "generated_on", nullable = false)
|
||||||
|
private LocalDateTime generatedOn;
|
||||||
|
|
||||||
|
@Column(name = "record_count", nullable = false)
|
||||||
|
private int recordCount;
|
||||||
|
|
||||||
|
@Column(name = "file_reference", length = 150)
|
||||||
|
private String fileReference;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "submitted_on")
|
||||||
|
private LocalDateTime submittedOn;
|
||||||
|
|
||||||
|
protected AmlCtrReport() {}
|
||||||
|
|
||||||
|
public static AmlCtrReport draft(String reportMonth, LocalDateTime generatedOn, int recordCount) {
|
||||||
|
final AmlCtrReport report = new AmlCtrReport();
|
||||||
|
report.reportMonth = reportMonth;
|
||||||
|
report.generatedOn = generatedOn;
|
||||||
|
report.recordCount = recordCount;
|
||||||
|
report.status = AmlCtrReportStatus.DRAFT.name();
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markGenerated(String fileReference) {
|
||||||
|
this.fileReference = fileReference;
|
||||||
|
this.status = AmlCtrReportStatus.GENERATED.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markSubmitted(LocalDateTime submittedOn) {
|
||||||
|
this.submittedOn = submittedOn;
|
||||||
|
this.status = AmlCtrReportStatus.SUBMITTED.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReportMonth() {
|
||||||
|
return reportMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getGeneratedOn() {
|
||||||
|
return generatedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRecordCount() {
|
||||||
|
return recordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileReference() {
|
||||||
|
return fileReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlCtrReportStatus getStatus() {
|
||||||
|
return AmlCtrReportStatus.valueOf(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getSubmittedOn() {
|
||||||
|
return submittedOn;
|
||||||
|
}
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One line per transaction (or connected-transaction group, when {@link #connectedGroup} is true)
|
||||||
|
* feeding a monthly {@link AmlCtrReport}.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_aml_ctr_report_line")
|
||||||
|
public class AmlCtrReportLine {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "ctr_report_id", nullable = false)
|
||||||
|
private AmlCtrReport ctrReport;
|
||||||
|
|
||||||
|
@Column(name = "transaction_type", nullable = false, length = 20)
|
||||||
|
private String transactionType;
|
||||||
|
|
||||||
|
@Column(name = "transaction_id")
|
||||||
|
private Long transactionId;
|
||||||
|
|
||||||
|
@Column(name = "client_id", nullable = false)
|
||||||
|
private Long clientId;
|
||||||
|
|
||||||
|
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@Column(name = "is_connected_group", nullable = false)
|
||||||
|
private boolean connectedGroup;
|
||||||
|
|
||||||
|
protected AmlCtrReportLine() {}
|
||||||
|
|
||||||
|
public static AmlCtrReportLine of(AmlCtrReport ctrReport, AmlTransactionType transactionType, Long transactionId, Long clientId,
|
||||||
|
BigDecimal amount, boolean connectedGroup) {
|
||||||
|
final AmlCtrReportLine line = new AmlCtrReportLine();
|
||||||
|
line.ctrReport = ctrReport;
|
||||||
|
line.transactionType = transactionType.name();
|
||||||
|
line.transactionId = transactionId;
|
||||||
|
line.clientId = clientId;
|
||||||
|
line.amount = amount;
|
||||||
|
line.connectedGroup = connectedGroup;
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlCtrReport getCtrReport() {
|
||||||
|
return ctrReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlTransactionType getTransactionType() {
|
||||||
|
return AmlTransactionType.valueOf(transactionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTransactionId() {
|
||||||
|
return transactionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getClientId() {
|
||||||
|
return clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConnectedGroup() {
|
||||||
|
return connectedGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface AmlCtrReportLineRepository extends JpaRepository<AmlCtrReportLine, Long> {
|
||||||
|
|
||||||
|
List<AmlCtrReportLine> findByCtrReport_Id(Long ctrReportId);
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface AmlCtrReportRepository extends JpaRepository<AmlCtrReport, Long> {
|
||||||
|
|
||||||
|
Optional<AmlCtrReport> findByReportMonth(String reportMonth);
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
public enum AmlCtrReportStatus {
|
||||||
|
|
||||||
|
DRAFT, GENERATED, SUBMITTED
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reviewer's judgement on a customer who repeatedly trips the AML engine.
|
||||||
|
*
|
||||||
|
* Distinct from the per-transaction disposition held on {@link AmlAlert}: that answers "was this
|
||||||
|
* transaction acceptable", this answers "is this customer's pattern of behaviour acceptable".
|
||||||
|
* {@link #alertCountAtReview} is what lets the queue clear — see the 0460 changelog.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_aml_recurring_review")
|
||||||
|
public class AmlRecurringReview {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@Column(name = "alert_count_at_review", nullable = false)
|
||||||
|
private Integer alertCountAtReview;
|
||||||
|
|
||||||
|
@Column(name = "outcome", nullable = false, length = 30)
|
||||||
|
private String outcome;
|
||||||
|
|
||||||
|
@Column(name = "notes", nullable = false, length = 1000)
|
||||||
|
private String notes;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
@JoinColumn(name = "reviewed_by", nullable = false)
|
||||||
|
private AppUser reviewedBy;
|
||||||
|
|
||||||
|
@Column(name = "reviewed_on", nullable = false)
|
||||||
|
private LocalDateTime reviewedOn;
|
||||||
|
|
||||||
|
protected AmlRecurringReview() {}
|
||||||
|
|
||||||
|
public static AmlRecurringReview record(final Client client, final int alertCountAtReview,
|
||||||
|
final AmlRecurringReviewOutcome outcome, final String notes, final AppUser reviewedBy) {
|
||||||
|
final AmlRecurringReview review = new AmlRecurringReview();
|
||||||
|
review.client = client;
|
||||||
|
review.alertCountAtReview = alertCountAtReview;
|
||||||
|
review.outcome = outcome.name();
|
||||||
|
review.notes = notes;
|
||||||
|
review.reviewedBy = reviewedBy;
|
||||||
|
review.reviewedOn = LocalDateTime.now();
|
||||||
|
return review;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getAlertCountAtReview() {
|
||||||
|
return alertCountAtReview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlRecurringReviewOutcome getOutcome() {
|
||||||
|
return AmlRecurringReviewOutcome.valueOf(outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNotes() {
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppUser getReviewedBy() {
|
||||||
|
return reviewedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getReviewedOn() {
|
||||||
|
return reviewedOn;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
/** What a reviewer concluded about a customer's repeated AML activity. */
|
||||||
|
public enum AmlRecurringReviewOutcome {
|
||||||
|
|
||||||
|
/** Behaviour explained and acceptable — nothing further, but the customer stays visible if it recurs. */
|
||||||
|
CLEARED,
|
||||||
|
/** Not conclusive; keep watching this customer more closely. */
|
||||||
|
MONITOR,
|
||||||
|
/** Warrants an STR case; the reviewer escalates the individual alerts separately. */
|
||||||
|
ESCALATED
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface AmlRecurringReviewRepository extends JpaRepository<AmlRecurringReview, Long> {
|
||||||
|
|
||||||
|
/** Newest first, so the caller can take the head as the latest review for a customer. */
|
||||||
|
List<AmlRecurringReview> findByClient_IdOrderByReviewedOnDesc(Long clientId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How an {@link AmlTransactionRule} is evaluated.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>THRESHOLD - a single transaction at or above {@code thresholdAmount}</li>
|
||||||
|
* <li>STRUCTURING - connected transactions summing at/above {@code thresholdAmount} within {@code windowDays}</li>
|
||||||
|
* <li>VELOCITY - unusual frequency of transactions within {@code windowDays}, independent of a fixed amount</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public enum AmlRuleType {
|
||||||
|
|
||||||
|
THRESHOLD, STRUCTURING, VELOCITY
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created when an {@link AmlAlert} is escalated past first-level review. {@link #concludedOn} is
|
||||||
|
* the Principal Officer's conclusion date - the RBI-mandated 7-working-day FIU filing clock
|
||||||
|
* ({@link #fiuDueDate}) starts here, never at the alert or transaction date.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_aml_str_case")
|
||||||
|
public class AmlStrCase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "alert_id", nullable = false)
|
||||||
|
private AmlAlert alert;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "assigned_po")
|
||||||
|
private AppUser assignedPo;
|
||||||
|
|
||||||
|
@Column(name = "investigation_notes", length = 2000)
|
||||||
|
private String investigationNotes;
|
||||||
|
|
||||||
|
@Column(name = "source_of_funds_notes", length = 2000)
|
||||||
|
private String sourceOfFundsNotes;
|
||||||
|
|
||||||
|
@Column(name = "conclusion", length = 20)
|
||||||
|
private String conclusion;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "concluded_by")
|
||||||
|
private AppUser concludedBy;
|
||||||
|
|
||||||
|
@Column(name = "concluded_on")
|
||||||
|
private LocalDate concludedOn;
|
||||||
|
|
||||||
|
@Column(name = "fiu_due_date")
|
||||||
|
private LocalDate fiuDueDate;
|
||||||
|
|
||||||
|
@Column(name = "str_filed_on")
|
||||||
|
private LocalDate strFiledOn;
|
||||||
|
|
||||||
|
@Column(name = "str_reference_no", length = 100)
|
||||||
|
private String strReferenceNo;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 25)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
protected AmlStrCase() {}
|
||||||
|
|
||||||
|
public static AmlStrCase openFor(AmlAlert alert) {
|
||||||
|
final AmlStrCase strCase = new AmlStrCase();
|
||||||
|
strCase.alert = alert;
|
||||||
|
strCase.client = alert.getClient();
|
||||||
|
strCase.status = AmlStrCaseStatus.OPEN.name();
|
||||||
|
return strCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assignPrincipalOfficer(AppUser po) {
|
||||||
|
this.assignedPo = po;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@code fiuDueDate} is computed by the caller using the working-days calendar, not here. */
|
||||||
|
public void conclude(AppUser po, AmlStrConclusion conclusion, String investigationNotes, String sourceOfFundsNotes,
|
||||||
|
LocalDate concludedOn, LocalDate fiuDueDate) {
|
||||||
|
this.concludedBy = po;
|
||||||
|
this.conclusion = conclusion.name();
|
||||||
|
this.investigationNotes = investigationNotes;
|
||||||
|
this.sourceOfFundsNotes = sourceOfFundsNotes;
|
||||||
|
this.concludedOn = concludedOn;
|
||||||
|
this.status = conclusion == AmlStrConclusion.SUSPICIOUS ? AmlStrCaseStatus.CONCLUDED_SUSPICIOUS.name()
|
||||||
|
: AmlStrCaseStatus.CONCLUDED_CLEAR.name();
|
||||||
|
this.fiuDueDate = conclusion == AmlStrConclusion.SUSPICIOUS ? fiuDueDate : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fileStr(String referenceNo, LocalDate filedOn) {
|
||||||
|
this.strReferenceNo = referenceNo;
|
||||||
|
this.strFiledOn = filedOn;
|
||||||
|
this.status = AmlStrCaseStatus.FILED.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getClientId() {
|
||||||
|
return client.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlAlert getAlert() {
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppUser getAssignedPo() {
|
||||||
|
return assignedPo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInvestigationNotes() {
|
||||||
|
return investigationNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSourceOfFundsNotes() {
|
||||||
|
return sourceOfFundsNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlStrConclusion getConclusion() {
|
||||||
|
return conclusion == null ? null : AmlStrConclusion.valueOf(conclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppUser getConcludedBy() {
|
||||||
|
return concludedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getConcludedOn() {
|
||||||
|
return concludedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getFiuDueDate() {
|
||||||
|
return fiuDueDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getStrFiledOn() {
|
||||||
|
return strFiledOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStrReferenceNo() {
|
||||||
|
return strReferenceNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlStrCaseStatus getStatus() {
|
||||||
|
return AmlStrCaseStatus.valueOf(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
public interface AmlStrCaseRepository extends JpaRepository<AmlStrCase, Long> {
|
||||||
|
|
||||||
|
long countByClient_Id(Long clientId);
|
||||||
|
|
||||||
|
List<AmlStrCase> findByStatus(String status);
|
||||||
|
|
||||||
|
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.concludedOn BETWEEN :from AND :to")
|
||||||
|
List<AmlStrCase> findConcludedSuspiciousBetween(@Param("from") LocalDate from, @Param("to") LocalDate to);
|
||||||
|
|
||||||
|
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.fiuDueDate < :asOf")
|
||||||
|
List<AmlStrCase> findOverdueForFiling(@Param("asOf") LocalDate asOf);
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
public enum AmlStrCaseStatus {
|
||||||
|
|
||||||
|
OPEN, CONCLUDED_CLEAR, CONCLUDED_SUSPICIOUS, FILED
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
/** The Principal Officer's final conclusion on an {@link AmlStrCase}. */
|
||||||
|
public enum AmlStrConclusion {
|
||||||
|
|
||||||
|
NOT_SUSPICIOUS, SUSPICIOUS
|
||||||
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A machine-evaluable transaction-monitoring rule. Distinct from {@code ClientRiskRule}
|
||||||
|
* (client-profile, KYC-time scoring) - these are checked against live savings/loan/transfer
|
||||||
|
* transactions as they post. {@link #sourceClientRiskRuleId} optionally traces a rule back to its
|
||||||
|
* originating entry in the 221-rule AML catalog seeded in {@code m_client_risk_rule}.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_aml_transaction_rule")
|
||||||
|
public class AmlTransactionRule {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "rule_code", nullable = false, length = 50)
|
||||||
|
private String ruleCode;
|
||||||
|
|
||||||
|
@Column(name = "description", nullable = false, length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "applies_to", nullable = false, length = 20)
|
||||||
|
private String appliesTo;
|
||||||
|
|
||||||
|
@Column(name = "threshold_amount", precision = 19, scale = 6)
|
||||||
|
private BigDecimal thresholdAmount;
|
||||||
|
|
||||||
|
/** Risk-category-tiered overrides of {@link #thresholdAmount}. Null on both means the rule is
|
||||||
|
* not tiered; {@link #thresholdAmount} is used for every client regardless of risk category. */
|
||||||
|
@Column(name = "threshold_amount_medium_risk", precision = 19, scale = 6)
|
||||||
|
private BigDecimal thresholdAmountMediumRisk;
|
||||||
|
|
||||||
|
@Column(name = "threshold_amount_high_risk", precision = 19, scale = 6)
|
||||||
|
private BigDecimal thresholdAmountHighRisk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declarative band the rule covers, shown on the AML Rules Master screen. Purely
|
||||||
|
* descriptive: unlike the tiered columns above these never take part in matching, so
|
||||||
|
* changing them cannot alter which transactions raise an alert. A null maximum means
|
||||||
|
* the rule is genuinely open-ended.
|
||||||
|
*/
|
||||||
|
@Column(name = "min_limit_amount", precision = 19, scale = 6)
|
||||||
|
private BigDecimal minLimitAmount;
|
||||||
|
|
||||||
|
@Column(name = "max_limit_amount", precision = 19, scale = 6)
|
||||||
|
private BigDecimal maxLimitAmount;
|
||||||
|
|
||||||
|
@Column(name = "rule_type", nullable = false, length = 20)
|
||||||
|
private String ruleType;
|
||||||
|
|
||||||
|
@Column(name = "window_days")
|
||||||
|
private Integer windowDays;
|
||||||
|
|
||||||
|
@Column(name = "source_client_risk_rule_id")
|
||||||
|
private Long sourceClientRiskRuleId;
|
||||||
|
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private boolean active;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CREDIT or DEBIT for a rule written about one direction only; null where either way qualifies.
|
||||||
|
* A rule named for withdrawals must not fire on a deposit.
|
||||||
|
*/
|
||||||
|
@Column(name = "direction", length = 10)
|
||||||
|
private String direction;
|
||||||
|
|
||||||
|
/** True where only physical cash qualifies — a cash-reporting rule is not about transfers. */
|
||||||
|
@Column(name = "cash_only", nullable = false)
|
||||||
|
private boolean cashOnly;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many transactions must make up the window before a pattern rule fires. Structuring is
|
||||||
|
* many small transactions; without this floor a single large one satisfies the window total and
|
||||||
|
* reads as a pattern it is not.
|
||||||
|
*/
|
||||||
|
@Column(name = "min_transaction_count")
|
||||||
|
private Integer minTransactionCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which customers a rule is about, where the rule is about the customer rather than the
|
||||||
|
* transaction: PEP, CHARITABLE_ORG, HIGH_RISK_BUSINESS, NEW_ACCOUNT. Null means the rule is
|
||||||
|
* about the transaction alone and so applies to everyone.
|
||||||
|
*/
|
||||||
|
@Column(name = "subject_scope", length = 30)
|
||||||
|
private String subjectScope;
|
||||||
|
|
||||||
|
public String getSubjectScope() {
|
||||||
|
return subjectScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDirection() {
|
||||||
|
return direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCashOnly() {
|
||||||
|
return cashOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMinTransactionCount() {
|
||||||
|
return minTransactionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AmlTransactionRule() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the live-transaction counterpart of a {@code ClientRiskRule} authored from the AML
|
||||||
|
* Rules Master screen. {@code sourceClientRiskRuleId} is the link the read side joins on, so
|
||||||
|
* the two rows are always created together and the rule code is derived from it.
|
||||||
|
*/
|
||||||
|
public static AmlTransactionRule createForRiskRule(final Long sourceClientRiskRuleId, final String description,
|
||||||
|
final AmlAppliesTo appliesTo, final AmlRuleType ruleType, final Integer windowDays,
|
||||||
|
final BigDecimal thresholdAmount, final BigDecimal minLimitAmount, final BigDecimal maxLimitAmount) {
|
||||||
|
final AmlTransactionRule rule = new AmlTransactionRule();
|
||||||
|
rule.sourceClientRiskRuleId = sourceClientRiskRuleId;
|
||||||
|
// rule_code is UNIQUE; the source rule id is unique per row, so this cannot collide.
|
||||||
|
rule.ruleCode = "CRR-" + sourceClientRiskRuleId;
|
||||||
|
rule.description = description;
|
||||||
|
rule.appliesTo = appliesTo.name();
|
||||||
|
rule.ruleType = ruleType.name();
|
||||||
|
rule.windowDays = windowDays;
|
||||||
|
rule.thresholdAmount = thresholdAmount;
|
||||||
|
rule.minLimitAmount = minLimitAmount;
|
||||||
|
rule.maxLimitAmount = maxLimitAmount;
|
||||||
|
rule.active = true;
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRuleCode() {
|
||||||
|
return ruleCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlAppliesTo getAppliesTo() {
|
||||||
|
return AmlAppliesTo.valueOf(appliesTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getThresholdAmount() {
|
||||||
|
return thresholdAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getThresholdAmountMediumRisk() {
|
||||||
|
return thresholdAmountMediumRisk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getThresholdAmountHighRisk() {
|
||||||
|
return thresholdAmountHighRisk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMinLimitAmount() {
|
||||||
|
return minLimitAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMaxLimitAmount() {
|
||||||
|
return maxLimitAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlRuleType getRuleType() {
|
||||||
|
return AmlRuleType.valueOf(ruleType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getWindowDays() {
|
||||||
|
return windowDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSourceClientRiskRuleId() {
|
||||||
|
return sourceClientRiskRuleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface AmlTransactionRuleRepository extends JpaRepository<AmlTransactionRule, Long> {
|
||||||
|
|
||||||
|
List<AmlTransactionRule> findByActiveTrueAndAppliesTo(String appliesTo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks a rule up by its code irrespective of {@code is_active}. Needed for MANUAL_REFERRAL, which is deliberately
|
||||||
|
* seeded inactive so the evaluation engine never fires it, yet must still be attachable to an alert a user raises
|
||||||
|
* by hand from the ledger.
|
||||||
|
*/
|
||||||
|
Optional<AmlTransactionRule> findByRuleCode(String ruleCode);
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
/** The kind of ledger transaction an {@link AmlAlert} was raised against. */
|
||||||
|
public enum AmlTransactionType {
|
||||||
|
|
||||||
|
SAVINGS, LOAN, TRANSFER
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable, append-only audit trail of a client's KYC risk-category changes. Backs the AML menu's
|
||||||
|
* existing "Risk Update History Report" item, which previously had no data behind it. Written both
|
||||||
|
* for manual re-categorization and for the automatic escalation triggered on a client's 3rd
|
||||||
|
* cumulative {@link AmlStrCase}.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_client_risk_category_history")
|
||||||
|
public class ClientRiskCategoryHistory {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@Column(name = "previous_category", nullable = false)
|
||||||
|
private Integer previousCategory;
|
||||||
|
|
||||||
|
@Column(name = "new_category", nullable = false)
|
||||||
|
private Integer newCategory;
|
||||||
|
|
||||||
|
@Column(name = "reason", nullable = false, length = 500)
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
@Column(name = "triggered_by", nullable = false, length = 30)
|
||||||
|
private String triggeredBy;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "str_case_id")
|
||||||
|
private AmlStrCase strCase;
|
||||||
|
|
||||||
|
@Column(name = "changed_on", nullable = false)
|
||||||
|
private LocalDateTime changedOn;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "changed_by", nullable = false)
|
||||||
|
private AppUser changedBy;
|
||||||
|
|
||||||
|
protected ClientRiskCategoryHistory() {}
|
||||||
|
|
||||||
|
public static ClientRiskCategoryHistory autoEscalation(Client client, Integer previousCategory, Integer newCategory,
|
||||||
|
AmlStrCase triggeringCase, AppUser systemActor, LocalDateTime changedOn) {
|
||||||
|
final ClientRiskCategoryHistory history = new ClientRiskCategoryHistory();
|
||||||
|
history.client = client;
|
||||||
|
history.previousCategory = previousCategory;
|
||||||
|
history.newCategory = newCategory;
|
||||||
|
history.reason = "Auto-escalated after client's 3rd cumulative AML STR case (case #" + triggeringCase.getId() + ")";
|
||||||
|
history.triggeredBy = RiskCategoryChangeTrigger.AML_AUTO_ESCALATION.name();
|
||||||
|
history.strCase = triggeringCase;
|
||||||
|
history.changedOn = changedOn;
|
||||||
|
history.changedBy = systemActor;
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClientRiskCategoryHistory manual(Client client, Integer previousCategory, Integer newCategory, String reason,
|
||||||
|
AppUser changedBy, LocalDateTime changedOn) {
|
||||||
|
final ClientRiskCategoryHistory history = new ClientRiskCategoryHistory();
|
||||||
|
history.client = client;
|
||||||
|
history.previousCategory = previousCategory;
|
||||||
|
history.newCategory = newCategory;
|
||||||
|
history.reason = reason;
|
||||||
|
history.triggeredBy = RiskCategoryChangeTrigger.MANUAL.name();
|
||||||
|
history.changedBy = changedBy;
|
||||||
|
history.changedOn = changedOn;
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPreviousCategory() {
|
||||||
|
return previousCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getNewCategory() {
|
||||||
|
return newCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RiskCategoryChangeTrigger getTriggeredBy() {
|
||||||
|
return RiskCategoryChangeTrigger.valueOf(triggeredBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AmlStrCase getStrCase() {
|
||||||
|
return strCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getChangedOn() {
|
||||||
|
return changedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppUser getChangedBy() {
|
||||||
|
return changedBy;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ClientRiskCategoryHistoryRepository extends JpaRepository<ClientRiskCategoryHistory, Long> {
|
||||||
|
|
||||||
|
List<ClientRiskCategoryHistory> findByClient_IdOrderByChangedOnDesc(Long clientId);
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
|
public enum RiskCategoryChangeTrigger {
|
||||||
|
|
||||||
|
AML_AUTO_ESCALATION, MANUAL
|
||||||
|
}
|
||||||
+2
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulator-side audit log for CFCFRMS (I4C/MHA) events.
|
* Simulator-side audit log for CFCFRMS (I4C/MHA) events.
|
||||||
@@ -69,7 +70,7 @@ public class CfcfrmsSimulatorLog extends AbstractAuditableWithUTCDateTimeCustom<
|
|||||||
this.eventType = eventType;
|
this.eventType = eventType;
|
||||||
this.payload = payload;
|
this.payload = payload;
|
||||||
this.responsePayload = responsePayload;
|
this.responsePayload = responsePayload;
|
||||||
this.eventTimestamp = OffsetDateTime.now();
|
this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc,
|
public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc,
|
||||||
|
|||||||
+2
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audit trail for every status change on a CFCFRMS complaint ticket.
|
* Audit trail for every status change on a CFCFRMS complaint ticket.
|
||||||
@@ -54,7 +55,7 @@ public class ComplaintAuditLog extends AbstractAuditableWithUTCDateTimeCustom<Lo
|
|||||||
this.event = event;
|
this.event = event;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.actor = actor;
|
this.actor = actor;
|
||||||
this.eventTimestamp = OffsetDateTime.now();
|
this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComplaintAuditLog(final String ticketId, final String event, final String description,
|
public ComplaintAuditLog(final String ticketId, final String event, final String description,
|
||||||
|
|||||||
+3
-2
@@ -26,6 +26,7 @@ import jakarta.persistence.Table;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CFCFRMS Complaint Ticket — represents a cyber fraud complaint pushed by MHA/I4C to the bank.
|
* CFCFRMS Complaint Ticket — represents a cyber fraud complaint pushed by MHA/I4C to the bank.
|
||||||
@@ -121,7 +122,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
ticket.status = ComplaintStatus.RECEIVED;
|
ticket.status = ComplaintStatus.RECEIVED;
|
||||||
ticket.assignedOfficer = assignedOfficer;
|
ticket.assignedOfficer = assignedOfficer;
|
||||||
ticket.statePoliceRef = statePoliceRef;
|
ticket.statePoliceRef = statePoliceRef;
|
||||||
ticket.receivedAt = OffsetDateTime.now();
|
ticket.receivedAt = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
return ticket;
|
return ticket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
this.status = newStatus;
|
this.status = newStatus;
|
||||||
this.actionTaken = actionNote;
|
this.actionTaken = actionNote;
|
||||||
if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) {
|
if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) {
|
||||||
this.resolvedAt = OffsetDateTime.now();
|
this.resolvedAt = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.adapter;
|
||||||
|
|
||||||
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.documentmanagement.adapter.EntitySignatureIdAdapter;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientRepository;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Component
|
||||||
|
class ClientSignatureIdAdapter implements EntitySignatureIdAdapter {
|
||||||
|
|
||||||
|
private static final String ENTITY_TYPE = "clients";
|
||||||
|
private static final String DOCUMENT_NAME = "clientSignature";
|
||||||
|
|
||||||
|
private final ClientRepository repository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean accept(String entityType, String documentName) {
|
||||||
|
return ENTITY_TYPE.equalsIgnoreCase(entityType) && DOCUMENT_NAME.equalsIgnoreCase(documentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Optional<SignatureIdResult> get(Long entityId) {
|
||||||
|
return repository.findById(entityId).filter(client -> nonNull(client.getSignatureId()))
|
||||||
|
.map(client -> SignatureIdResult.builder().id(client.getSignatureId()).displayName(client.getDisplayName()).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Optional<SignatureIdResult> set(Long entityId, Long signatureId) {
|
||||||
|
final var result = get(entityId);
|
||||||
|
|
||||||
|
if (signatureId == null) {
|
||||||
|
repository.removeSignatureId(entityId);
|
||||||
|
} else {
|
||||||
|
repository.updateByIdAndSignatureId(entityId, signatureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-2
@@ -63,6 +63,7 @@ public class ClientApiConstants {
|
|||||||
public static final String CLIENT_CLASSIFICATION = "ClientClassification";
|
public static final String CLIENT_CLASSIFICATION = "ClientClassification";
|
||||||
|
|
||||||
public static final String OCCUPATION_CATEGORY = "OccupationCategory";
|
public static final String OCCUPATION_CATEGORY = "OccupationCategory";
|
||||||
|
public static final String PWD_CATEGORY = "PwdCategory";
|
||||||
public static final String OCCUPATION = "Occupation";
|
public static final String OCCUPATION = "Occupation";
|
||||||
|
|
||||||
public static final String CLIENT_NON_PERSON_CONSTITUTION = "Constitution";
|
public static final String CLIENT_NON_PERSON_CONSTITUTION = "Constitution";
|
||||||
@@ -96,6 +97,15 @@ public class ClientApiConstants {
|
|||||||
public static final String mobileNoParamName = "mobileNo";
|
public static final String mobileNoParamName = "mobileNo";
|
||||||
public static final String alternateMobileNoParamName = "alternateMobileNo";
|
public static final String alternateMobileNoParamName = "alternateMobileNo";
|
||||||
public static final String emailAddressParamName = "emailAddress";
|
public static final String emailAddressParamName = "emailAddress";
|
||||||
|
// Dual-OTP contact-change verification (see ClientContactOtpService) — required only when
|
||||||
|
// mobileNo or emailAddress is being changed via PUT /clients/{id}. Reference an already
|
||||||
|
// VERIFIED m_otp_log row for the OLD destination (current DB value) and the NEW destination
|
||||||
|
// (incoming value) respectively. Field-specific (not a single generic pair) so mobileNo and
|
||||||
|
// emailAddress can both change in the same request without ambiguity.
|
||||||
|
public static final String mobileOldOtpIdParamName = "mobileOldOtpId";
|
||||||
|
public static final String mobileNewOtpIdParamName = "mobileNewOtpId";
|
||||||
|
public static final String emailOldOtpIdParamName = "emailOldOtpId";
|
||||||
|
public static final String emailNewOtpIdParamName = "emailNewOtpId";
|
||||||
public static final String firstnameParamName = "firstname";
|
public static final String firstnameParamName = "firstname";
|
||||||
public static final String middlenameParamName = "middlename";
|
public static final String middlenameParamName = "middlename";
|
||||||
public static final String lastnameParamName = "lastname";
|
public static final String lastnameParamName = "lastname";
|
||||||
@@ -138,7 +148,13 @@ public class ClientApiConstants {
|
|||||||
public static final String religionParamName = "religion";
|
public static final String religionParamName = "religion";
|
||||||
public static final String socialCategoryParamName = "socialCategory";
|
public static final String socialCategoryParamName = "socialCategory";
|
||||||
public static final String monthlyIncomeParamName = "monthlyIncome";
|
public static final String monthlyIncomeParamName = "monthlyIncome";
|
||||||
|
public static final String annualIncomeParamName = "annualIncome";
|
||||||
public static final String annualTurnoverParamName = "annualTurnover";
|
public static final String annualTurnoverParamName = "annualTurnover";
|
||||||
|
public static final String monthlyTurnoverParamName = "monthlyTurnover";
|
||||||
|
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
||||||
|
public static final String isPwdParamName = "isPwd";
|
||||||
|
public static final String pwdCategoryIdParamName = "pwdCategoryId";
|
||||||
|
public static final String pwdCategoryParamName = "pwdCategory";
|
||||||
public static final String agricultureIncomeParamName = "agricultureIncome";
|
public static final String agricultureIncomeParamName = "agricultureIncome";
|
||||||
public static final String nonAgricultureIncomeParamName = "nonAgricultureIncome";
|
public static final String nonAgricultureIncomeParamName = "nonAgricultureIncome";
|
||||||
public static final String ageYearsParamName = "ageYears";
|
public static final String ageYearsParamName = "ageYears";
|
||||||
@@ -146,6 +162,15 @@ public class ClientApiConstants {
|
|||||||
public static final String ageDaysParamName = "ageDays";
|
public static final String ageDaysParamName = "ageDays";
|
||||||
public static final String nationalityIdParamName = "nationalityId";
|
public static final String nationalityIdParamName = "nationalityId";
|
||||||
public static final String isMinorParamName = "isMinor";
|
public static final String isMinorParamName = "isMinor";
|
||||||
|
/**
|
||||||
|
* Below this age a client is treated as a minor: guardian mapping required and a restricted KYC/OVD
|
||||||
|
* document set. Must stay in step with MINOR_AGE_THRESHOLD in the web app's client-age util -- the
|
||||||
|
* KYC template endpoint is authoritative for the document list, so a mismatch would serve an adult
|
||||||
|
* list (with mandatory PAN) for a client the UI classifies as a minor.
|
||||||
|
*/
|
||||||
|
public static final int MINOR_AGE_THRESHOLD = 10;
|
||||||
|
/** At or above this age a client qualifies for senior-citizen deposit rates. */
|
||||||
|
public static final int SENIOR_CITIZEN_AGE_THRESHOLD = 60;
|
||||||
public static final String guardianClientIdParamName = "guardianClientId";
|
public static final String guardianClientIdParamName = "guardianClientId";
|
||||||
public static final String guardianClientDisplayNameParamName = "guardianClientDisplayName";
|
public static final String guardianClientDisplayNameParamName = "guardianClientDisplayName";
|
||||||
public static final String guardianRelationParamName = "guardianRelation";
|
public static final String guardianRelationParamName = "guardianRelation";
|
||||||
@@ -175,6 +200,7 @@ public class ClientApiConstants {
|
|||||||
public static final String constitutionIdParamName = "constitutionId";
|
public static final String constitutionIdParamName = "constitutionId";
|
||||||
public static final String mainBusinessLineIdParamName = "mainBusinessLineId";
|
public static final String mainBusinessLineIdParamName = "mainBusinessLineId";
|
||||||
public static final String authorizedSignatoryParamName = "authorizedSignatory";
|
public static final String authorizedSignatoryParamName = "authorizedSignatory";
|
||||||
|
public static final String authorizedSignatoryClientIdsParamName = "authorizedSignatoryClientIds";
|
||||||
public static final String additionalInfoParamName = "additionalInfo";
|
public static final String additionalInfoParamName = "additionalInfo";
|
||||||
public static final String dateOfRegistrationParamName = "dateOfRegistration";
|
public static final String dateOfRegistrationParamName = "dateOfRegistration";
|
||||||
|
|
||||||
@@ -248,8 +274,9 @@ public class ClientApiConstants {
|
|||||||
staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName,
|
staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName,
|
||||||
salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName,
|
salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName,
|
||||||
educationLevelParamName, degreeQualificationParamName,
|
educationLevelParamName, degreeQualificationParamName,
|
||||||
religionParamName, socialCategoryParamName, monthlyIncomeParamName,
|
religionParamName, socialCategoryParamName, monthlyIncomeParamName, annualIncomeParamName,
|
||||||
annualTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
annualTurnoverParamName, monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName,
|
||||||
|
isSeniorCitizenParamName, isPwdParamName, pwdCategoryParamName,
|
||||||
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
|
||||||
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName,
|
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName,
|
||||||
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
|
||||||
|
|||||||
@@ -82,7 +82,13 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
private CodeValueData religion;
|
private CodeValueData religion;
|
||||||
private CodeValueData socialCategory;
|
private CodeValueData socialCategory;
|
||||||
private BigDecimal monthlyIncome;
|
private BigDecimal monthlyIncome;
|
||||||
|
private BigDecimal annualIncome;
|
||||||
private BigDecimal annualTurnover;
|
private BigDecimal annualTurnover;
|
||||||
|
private BigDecimal monthlyTurnover;
|
||||||
|
private Boolean isSeniorCitizen;
|
||||||
|
private Boolean isPwd;
|
||||||
|
private CodeValueData pwdCategory;
|
||||||
|
private Collection<CodeValueData> pwdCategoryOptions;
|
||||||
private BigDecimal agricultureIncome;
|
private BigDecimal agricultureIncome;
|
||||||
private BigDecimal nonAgricultureIncome;
|
private BigDecimal nonAgricultureIncome;
|
||||||
private Integer ageYears;
|
private Integer ageYears;
|
||||||
@@ -99,6 +105,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
private Boolean alternateDipVerified;
|
private Boolean alternateDipVerified;
|
||||||
private Long alternateDipLookupLogId;
|
private Long alternateDipLookupLogId;
|
||||||
private String alternateDipTimestamp;
|
private String alternateDipTimestamp;
|
||||||
|
/**
|
||||||
|
* Live DIP/MNRL status of the number, resolved from the latest lookup rather than the flag
|
||||||
|
* captured at onboarding. A customer cleared at onboarding whose number is later reported or
|
||||||
|
* reassigned would otherwise keep showing "Verified" forever.
|
||||||
|
*/
|
||||||
|
private String dipLiveStatus;
|
||||||
|
private String alternateDipLiveStatus;
|
||||||
|
/**
|
||||||
|
* Disposition of the client's most recent flagged sanctions alert, or null when they carry
|
||||||
|
* none. Like the DIP status this is resolved live: nameVerified is captured at onboarding and
|
||||||
|
* would keep reading "Clear" after the client was added to a later list snapshot.
|
||||||
|
*/
|
||||||
|
private String sanctionsAlertStatus;
|
||||||
private Boolean nameVerified;
|
private Boolean nameVerified;
|
||||||
private Long screeningLogId;
|
private Long screeningLogId;
|
||||||
private Boolean isStaff;
|
private Boolean isStaff;
|
||||||
@@ -400,13 +419,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
|
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
|
||||||
data.religionOptions = templateData.religionOptions;
|
data.religionOptions = templateData.religionOptions;
|
||||||
data.socialCategoryOptions = templateData.socialCategoryOptions;
|
data.socialCategoryOptions = templateData.socialCategoryOptions;
|
||||||
|
data.pwdCategoryOptions = templateData.pwdCategoryOptions;
|
||||||
data.maritalStatus = clientData.maritalStatus;
|
data.maritalStatus = clientData.maritalStatus;
|
||||||
data.educationLevel = clientData.educationLevel;
|
data.educationLevel = clientData.educationLevel;
|
||||||
data.degreeQualification = clientData.degreeQualification;
|
data.degreeQualification = clientData.degreeQualification;
|
||||||
data.religion = clientData.religion;
|
data.religion = clientData.religion;
|
||||||
data.socialCategory = clientData.socialCategory;
|
data.socialCategory = clientData.socialCategory;
|
||||||
data.monthlyIncome = clientData.monthlyIncome;
|
data.monthlyIncome = clientData.monthlyIncome;
|
||||||
|
data.annualIncome = clientData.annualIncome;
|
||||||
data.annualTurnover = clientData.annualTurnover;
|
data.annualTurnover = clientData.annualTurnover;
|
||||||
|
data.monthlyTurnover = clientData.monthlyTurnover;
|
||||||
|
data.isSeniorCitizen = clientData.isSeniorCitizen;
|
||||||
|
data.isPwd = clientData.isPwd;
|
||||||
|
data.pwdCategory = clientData.pwdCategory;
|
||||||
data.agricultureIncome = clientData.agricultureIncome;
|
data.agricultureIncome = clientData.agricultureIncome;
|
||||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||||
data.ageYears = clientData.ageYears;
|
data.ageYears = clientData.ageYears;
|
||||||
@@ -424,6 +449,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.alternateDipVerified = clientData.alternateDipVerified;
|
data.alternateDipVerified = clientData.alternateDipVerified;
|
||||||
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
||||||
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
||||||
|
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
|
||||||
|
// here is silently dropped from the payload the client view loads.
|
||||||
|
data.dipLiveStatus = clientData.dipLiveStatus;
|
||||||
|
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
|
||||||
|
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
|
||||||
data.nameVerified = clientData.nameVerified;
|
data.nameVerified = clientData.nameVerified;
|
||||||
data.screeningLogId = clientData.screeningLogId;
|
data.screeningLogId = clientData.screeningLogId;
|
||||||
return data;
|
return data;
|
||||||
@@ -456,13 +486,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
|
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
|
||||||
data.religionOptions = clientData.religionOptions;
|
data.religionOptions = clientData.religionOptions;
|
||||||
data.socialCategoryOptions = clientData.socialCategoryOptions;
|
data.socialCategoryOptions = clientData.socialCategoryOptions;
|
||||||
|
data.pwdCategoryOptions = clientData.pwdCategoryOptions;
|
||||||
data.maritalStatus = clientData.maritalStatus;
|
data.maritalStatus = clientData.maritalStatus;
|
||||||
data.educationLevel = clientData.educationLevel;
|
data.educationLevel = clientData.educationLevel;
|
||||||
data.degreeQualification = clientData.degreeQualification;
|
data.degreeQualification = clientData.degreeQualification;
|
||||||
data.religion = clientData.religion;
|
data.religion = clientData.religion;
|
||||||
data.socialCategory = clientData.socialCategory;
|
data.socialCategory = clientData.socialCategory;
|
||||||
data.monthlyIncome = clientData.monthlyIncome;
|
data.monthlyIncome = clientData.monthlyIncome;
|
||||||
|
data.annualIncome = clientData.annualIncome;
|
||||||
data.annualTurnover = clientData.annualTurnover;
|
data.annualTurnover = clientData.annualTurnover;
|
||||||
|
data.monthlyTurnover = clientData.monthlyTurnover;
|
||||||
|
data.isSeniorCitizen = clientData.isSeniorCitizen;
|
||||||
|
data.isPwd = clientData.isPwd;
|
||||||
|
data.pwdCategory = clientData.pwdCategory;
|
||||||
data.agricultureIncome = clientData.agricultureIncome;
|
data.agricultureIncome = clientData.agricultureIncome;
|
||||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||||
data.ageYears = clientData.ageYears;
|
data.ageYears = clientData.ageYears;
|
||||||
@@ -480,6 +516,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.alternateDipVerified = clientData.alternateDipVerified;
|
data.alternateDipVerified = clientData.alternateDipVerified;
|
||||||
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
||||||
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
||||||
|
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
|
||||||
|
// here is silently dropped from the payload the client view loads.
|
||||||
|
data.dipLiveStatus = clientData.dipLiveStatus;
|
||||||
|
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
|
||||||
|
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
|
||||||
data.nameVerified = clientData.nameVerified;
|
data.nameVerified = clientData.nameVerified;
|
||||||
data.screeningLogId = clientData.screeningLogId;
|
data.screeningLogId = clientData.screeningLogId;
|
||||||
return data;
|
return data;
|
||||||
@@ -509,7 +550,12 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.religion = clientData.religion;
|
data.religion = clientData.religion;
|
||||||
data.socialCategory = clientData.socialCategory;
|
data.socialCategory = clientData.socialCategory;
|
||||||
data.monthlyIncome = clientData.monthlyIncome;
|
data.monthlyIncome = clientData.monthlyIncome;
|
||||||
|
data.annualIncome = clientData.annualIncome;
|
||||||
data.annualTurnover = clientData.annualTurnover;
|
data.annualTurnover = clientData.annualTurnover;
|
||||||
|
data.monthlyTurnover = clientData.monthlyTurnover;
|
||||||
|
data.isSeniorCitizen = clientData.isSeniorCitizen;
|
||||||
|
data.isPwd = clientData.isPwd;
|
||||||
|
data.pwdCategory = clientData.pwdCategory;
|
||||||
data.agricultureIncome = clientData.agricultureIncome;
|
data.agricultureIncome = clientData.agricultureIncome;
|
||||||
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
data.nonAgricultureIncome = clientData.nonAgricultureIncome;
|
||||||
data.ageYears = clientData.ageYears;
|
data.ageYears = clientData.ageYears;
|
||||||
@@ -527,6 +573,11 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
|||||||
data.alternateDipVerified = clientData.alternateDipVerified;
|
data.alternateDipVerified = clientData.alternateDipVerified;
|
||||||
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
data.alternateDipLookupLogId = clientData.alternateDipLookupLogId;
|
||||||
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
data.alternateDipTimestamp = clientData.alternateDipTimestamp;
|
||||||
|
// Carried across too: these helpers rebuild ClientData field by field, so a field missing
|
||||||
|
// here is silently dropped from the payload the client view loads.
|
||||||
|
data.dipLiveStatus = clientData.dipLiveStatus;
|
||||||
|
data.alternateDipLiveStatus = clientData.alternateDipLiveStatus;
|
||||||
|
data.sanctionsAlertStatus = clientData.sanctionsAlertStatus;
|
||||||
data.nameVerified = clientData.nameVerified;
|
data.nameVerified = clientData.nameVerified;
|
||||||
data.screeningLogId = clientData.screeningLogId;
|
data.screeningLogId = clientData.screeningLogId;
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.fineract.portfolio.client.data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public final class ClientFamilyMemberChildData implements Serializable {
|
||||||
|
private final Long id;
|
||||||
|
private final String fullName;
|
||||||
|
private final Long genderId;
|
||||||
|
private final String gender;
|
||||||
|
private final LocalDate dateOfBirth;
|
||||||
|
}
|
||||||
+3
@@ -84,6 +84,9 @@ public final class ClientFamilyMembersData implements Serializable {
|
|||||||
private final LocalDate marriageDate;
|
private final LocalDate marriageDate;
|
||||||
private final String marriageAuthority;
|
private final String marriageAuthority;
|
||||||
|
|
||||||
|
@lombok.Setter
|
||||||
|
private java.util.List<ClientFamilyMemberChildData> children;
|
||||||
|
|
||||||
// template holder
|
// template holder
|
||||||
private final Collection<CodeValueData> relationshipIdOptions;
|
private final Collection<CodeValueData> relationshipIdOptions;
|
||||||
private final Collection<CodeValueData> genderIdOptions;
|
private final Collection<CodeValueData> genderIdOptions;
|
||||||
|
|||||||
+14
@@ -34,6 +34,8 @@ public class ClientNonPersonData implements Serializable {
|
|||||||
private final CodeValueData mainBusinessLine;
|
private final CodeValueData mainBusinessLine;
|
||||||
private final String remarks;
|
private final String remarks;
|
||||||
private final String authorizedSignatory;
|
private final String authorizedSignatory;
|
||||||
|
/** Client ids behind {@link #authorizedSignatory}, comma separated and in the same order. */
|
||||||
|
private final String authorizedSignatoryClientIds;
|
||||||
private final String additionalInfo;
|
private final String additionalInfo;
|
||||||
private final LocalDate dateOfRegistration;
|
private final LocalDate dateOfRegistration;
|
||||||
|
|
||||||
@@ -62,13 +64,24 @@ public class ClientNonPersonData implements Serializable {
|
|||||||
this.constitution = null;
|
this.constitution = null;
|
||||||
this.mainBusinessLine = null;
|
this.mainBusinessLine = null;
|
||||||
this.authorizedSignatory = null;
|
this.authorizedSignatory = null;
|
||||||
|
this.authorizedSignatoryClientIds = null;
|
||||||
this.additionalInfo = null;
|
this.additionalInfo = null;
|
||||||
this.dateOfRegistration = null;
|
this.dateOfRegistration = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kept for the list/search mappers, whose SQL does not select the signatory client ids.
|
||||||
|
*/
|
||||||
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
|
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
|
||||||
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
|
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
|
||||||
LocalDate dateOfRegistration) {
|
LocalDate dateOfRegistration) {
|
||||||
|
this(constitution, incorpNo, incorpValidityTillDate, mainBusinessLine, remarks, authorizedSignatory, additionalInfo,
|
||||||
|
dateOfRegistration, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientNonPersonData(CodeValueData constitution, String incorpNo, LocalDate incorpValidityTillDate,
|
||||||
|
CodeValueData mainBusinessLine, String remarks, String authorizedSignatory, String additionalInfo,
|
||||||
|
LocalDate dateOfRegistration, String authorizedSignatoryClientIds) {
|
||||||
|
|
||||||
this.constitution = constitution;
|
this.constitution = constitution;
|
||||||
this.incorpNumber = incorpNo;
|
this.incorpNumber = incorpNo;
|
||||||
@@ -76,6 +89,7 @@ public class ClientNonPersonData implements Serializable {
|
|||||||
this.mainBusinessLine = mainBusinessLine;
|
this.mainBusinessLine = mainBusinessLine;
|
||||||
this.remarks = remarks;
|
this.remarks = remarks;
|
||||||
this.authorizedSignatory = authorizedSignatory;
|
this.authorizedSignatory = authorizedSignatory;
|
||||||
|
this.authorizedSignatoryClientIds = authorizedSignatoryClientIds;
|
||||||
this.additionalInfo = additionalInfo;
|
this.additionalInfo = additionalInfo;
|
||||||
this.dateOfRegistration = dateOfRegistration;
|
this.dateOfRegistration = dateOfRegistration;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import jakarta.persistence.JoinTable;
|
|||||||
import jakarta.persistence.ManyToMany;
|
import jakarta.persistence.ManyToMany;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.OneToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import jakarta.persistence.UniqueConstraint;
|
import jakarta.persistence.UniqueConstraint;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -77,6 +78,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "image_id")
|
@Column(name = "image_id")
|
||||||
private Long imageId;
|
private Long imageId;
|
||||||
|
|
||||||
|
@Column(name = "signature_id")
|
||||||
|
private Long signatureId;
|
||||||
|
|
||||||
@Column(name = "status_enum", nullable = false)
|
@Column(name = "status_enum", nullable = false)
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
@@ -231,9 +235,28 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "monthly_income")
|
@Column(name = "monthly_income")
|
||||||
private java.math.BigDecimal monthlyIncome;
|
private java.math.BigDecimal monthlyIncome;
|
||||||
|
|
||||||
|
@Column(name = "annual_income")
|
||||||
|
private java.math.BigDecimal annualIncome;
|
||||||
|
|
||||||
@Column(name = "annual_turnover")
|
@Column(name = "annual_turnover")
|
||||||
private java.math.BigDecimal annualTurnover;
|
private java.math.BigDecimal annualTurnover;
|
||||||
|
|
||||||
|
@Column(name = "monthly_turnover")
|
||||||
|
private java.math.BigDecimal monthlyTurnover;
|
||||||
|
|
||||||
|
@Column(name = "is_senior_citizen")
|
||||||
|
private Boolean isSeniorCitizen;
|
||||||
|
|
||||||
|
@Column(name = "is_pwd")
|
||||||
|
private Boolean isPwd;
|
||||||
|
|
||||||
|
@OneToOne(mappedBy = "client", fetch = FetchType.LAZY)
|
||||||
|
private ClientKyc clientKyc;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "pwd_category_cv_id")
|
||||||
|
private CodeValue pwdCategory;
|
||||||
|
|
||||||
@Column(name = "agriculture_income")
|
@Column(name = "agriculture_income")
|
||||||
private java.math.BigDecimal agricultureIncome;
|
private java.math.BigDecimal agricultureIncome;
|
||||||
|
|
||||||
@@ -271,6 +294,17 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "alternate_dip_timestamp")
|
@Column(name = "alternate_dip_timestamp")
|
||||||
private String alternateDipTimestamp;
|
private String alternateDipTimestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DIP/MNRL status the number actually returned — CLEAN, SUSPECTED, DISCONNECTED or
|
||||||
|
* SERVICE_UNAVAILABLE. dipVerified alone cannot tell "checked and clean" apart from "the DoT
|
||||||
|
* service was down", which previously stored both as verified.
|
||||||
|
*/
|
||||||
|
@Column(name = "dip_status", length = 40)
|
||||||
|
private String dipStatus;
|
||||||
|
|
||||||
|
@Column(name = "alternate_dip_status", length = 40)
|
||||||
|
private String alternateDipStatus;
|
||||||
|
|
||||||
@Column(name = "name_verified")
|
@Column(name = "name_verified")
|
||||||
private Boolean nameVerified;
|
private Boolean nameVerified;
|
||||||
|
|
||||||
@@ -598,6 +632,10 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return this.office.getId();
|
return this.office.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return this.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
public String mobileNo() {
|
public String mobileNo() {
|
||||||
return this.mobileNo;
|
return this.mobileNo;
|
||||||
}
|
}
|
||||||
@@ -832,6 +870,14 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return this.socialCategory != null ? this.socialCategory.getId() : null;
|
return this.socialCategory != null ? this.socialCategory.getId() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long pwdCategoryId() {
|
||||||
|
return this.pwdCategory != null ? this.pwdCategory.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updatePwdCategory(CodeValue pwdCategory) {
|
||||||
|
this.pwdCategory = pwdCategory;
|
||||||
|
}
|
||||||
|
|
||||||
public void updateSocialCategory(CodeValue socialCategory) {
|
public void updateSocialCategory(CodeValue socialCategory) {
|
||||||
this.socialCategory = socialCategory;
|
this.socialCategory = socialCategory;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -29,6 +29,7 @@ import lombok.Getter;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form 60 declaration as per Rule 114B of Income Tax Rules, 1962.
|
* Form 60 declaration as per Rule 114B of Income Tax Rules, 1962.
|
||||||
@@ -87,7 +88,7 @@ public class ClientForm60 extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
form.estimatedTotalIncome = command.bigDecimalValueOfParameterNamed("estimatedTotalIncome");
|
form.estimatedTotalIncome = command.bigDecimalValueOfParameterNamed("estimatedTotalIncome");
|
||||||
form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null);
|
form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null);
|
||||||
form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration");
|
form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration");
|
||||||
form.submittedOn = LocalDate.now();
|
form.submittedOn = DateUtils.getLocalDateOfTenant();
|
||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+426
-11
@@ -25,13 +25,19 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.Transient;
|
||||||
import jakarta.persistence.UniqueConstraint;
|
import jakarta.persistence.UniqueConstraint;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a client's KYC (Know Your Customer) record as mandated by the Reserve Bank of India
|
* Represents a client's KYC (Know Your Customer) record as mandated by the Reserve Bank of India
|
||||||
@@ -59,6 +65,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@JoinColumn(name = "client_id", nullable = false)
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
private Client client;
|
private Client client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scratch record of what the in-flight {@link #update} replaced, read back via
|
||||||
|
* {@link #getFieldsOverwrittenByLastUpdate()}. Never persisted and never read outside the
|
||||||
|
* transaction that called update(), which clears it first.
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
private final transient Set<String> fieldsOverwrittenByLastUpdate = new LinkedHashSet<>();
|
||||||
|
|
||||||
/** 100 = PENDING, 200 = IN_PROGRESS, 300 = VERIFIED, 400 = REJECTED, 500 = EXPIRED */
|
/** 100 = PENDING, 200 = IN_PROGRESS, 300 = VERIFIED, 400 = REJECTED, 500 = EXPIRED */
|
||||||
@Column(name = "kyc_status", nullable = false)
|
@Column(name = "kyc_status", nullable = false)
|
||||||
private Integer kycStatus;
|
private Integer kycStatus;
|
||||||
@@ -102,6 +116,36 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "pan_status", length = 20)
|
@Column(name = "pan_status", length = 20)
|
||||||
private String panStatus;
|
private String panStatus;
|
||||||
|
|
||||||
|
// ── PAN declaration ──────────────────────────────────────────────────────
|
||||||
|
// Accepted in place of a passing PAN check. The Income Tax check matches on name AND date of
|
||||||
|
// birth, so a customer whose PAN card carries a different spelling — or a different DOB — can
|
||||||
|
// never clear it however many times the officer retries. Rather than leave such a client
|
||||||
|
// permanently in KYC exception, the branch takes a signed declaration recording what differs.
|
||||||
|
// Holding that declaration is what suppresses the exception, so it is stored, not just shown.
|
||||||
|
|
||||||
|
/** True once a signed declaration has been accepted for a PAN that could not be verified. */
|
||||||
|
@Column(name = "pan_declaration_taken")
|
||||||
|
private Boolean panDeclarationTaken;
|
||||||
|
|
||||||
|
/** What the declaration covers: "NAME", "DOB", "NAME,DOB", or "NONE" when PAN simply failed. */
|
||||||
|
@Column(name = "pan_declaration_mismatch_fields", length = 100)
|
||||||
|
private String panDeclarationMismatchFields;
|
||||||
|
|
||||||
|
/** Name exactly as printed on the physical PAN card, keyed in by the officer. */
|
||||||
|
@Column(name = "pan_declaration_name_on_pan", length = 200)
|
||||||
|
private String panDeclarationNameOnPan;
|
||||||
|
|
||||||
|
/** Date of birth exactly as printed on the physical PAN card. */
|
||||||
|
@Column(name = "pan_declaration_dob_on_pan")
|
||||||
|
private LocalDate panDeclarationDobOnPan;
|
||||||
|
|
||||||
|
/** Officer's free-text note — why the mismatch exists and what proof was sighted. */
|
||||||
|
@Column(name = "pan_declaration_remarks", length = 500)
|
||||||
|
private String panDeclarationRemarks;
|
||||||
|
|
||||||
|
@Column(name = "pan_declaration_taken_on")
|
||||||
|
private LocalDate panDeclarationTakenOn;
|
||||||
|
|
||||||
// ── Aadhaar ──────────────────────────────────────────────────────────────
|
// ── Aadhaar ──────────────────────────────────────────────────────────────
|
||||||
/** Stored masked (last 4 digits visible). Full number must never be persisted as per UIDAI directive. */
|
/** Stored masked (last 4 digits visible). Full number must never be persisted as per UIDAI directive. */
|
||||||
@Column(name = "aadhaar_number_masked", length = 12)
|
@Column(name = "aadhaar_number_masked", length = 12)
|
||||||
@@ -113,6 +157,31 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "aadhaar_verified_on")
|
@Column(name = "aadhaar_verified_on")
|
||||||
private LocalDate aadhaarVerifiedOn;
|
private LocalDate aadhaarVerifiedOn;
|
||||||
|
|
||||||
|
// ── Aadhaar enrolment receipt ────────────────────────────────────────────
|
||||||
|
// The counterpart of the PAN declaration, for a customer who has applied for Aadhaar but does
|
||||||
|
// not hold one yet. UIDAI issues an enrolment slip carrying a 28-digit EID, and until Aadhaar
|
||||||
|
// itself is issued there is no number to put through the OTP flow — the check cannot pass
|
||||||
|
// however long the branch waits. Recording the receipt is what waives the exception.
|
||||||
|
|
||||||
|
/** True once the customer's Aadhaar enrolment slip has been sighted and recorded. */
|
||||||
|
@Column(name = "aadhaar_receipt_taken")
|
||||||
|
private Boolean aadhaarReceiptTaken;
|
||||||
|
|
||||||
|
/** 28-digit Enrolment ID from the slip, stored with its printed separators. */
|
||||||
|
@Column(name = "aadhaar_enrolment_id", length = 40)
|
||||||
|
private String aadhaarEnrolmentId;
|
||||||
|
|
||||||
|
/** Enrolment date printed on the slip — a receipt is temporary, so its age matters. */
|
||||||
|
@Column(name = "aadhaar_enrolment_date")
|
||||||
|
private LocalDate aadhaarEnrolmentDate;
|
||||||
|
|
||||||
|
/** Officer's note on what was sighted. */
|
||||||
|
@Column(name = "aadhaar_receipt_remarks", length = 500)
|
||||||
|
private String aadhaarReceiptRemarks;
|
||||||
|
|
||||||
|
@Column(name = "aadhaar_receipt_taken_on")
|
||||||
|
private LocalDate aadhaarReceiptTakenOn;
|
||||||
|
|
||||||
// ── GSTIN (mandatory for NON_INDIVIDUAL clients) ─────────────────────────
|
// ── GSTIN (mandatory for NON_INDIVIDUAL clients) ─────────────────────────
|
||||||
@Column(name = "gstin_number", length = 15)
|
@Column(name = "gstin_number", length = 15)
|
||||||
private String gstinNumber;
|
private String gstinNumber;
|
||||||
@@ -194,6 +263,18 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "freeze_authority", length = 100)
|
@Column(name = "freeze_authority", length = 100)
|
||||||
private String freezeAuthority;
|
private String freezeAuthority;
|
||||||
|
|
||||||
|
@Column(name = "freeze_date")
|
||||||
|
private LocalDate freezeDate;
|
||||||
|
|
||||||
|
@Column(name = "freeze_amount", precision = 19, scale = 6)
|
||||||
|
private BigDecimal freezeAmount;
|
||||||
|
|
||||||
|
@Column(name = "unfreeze_date")
|
||||||
|
private LocalDate unfreezeDate;
|
||||||
|
|
||||||
|
@Column(name = "no_debit_date")
|
||||||
|
private LocalDate noDebitDate;
|
||||||
|
|
||||||
// ── Form 60 / Form 15G/15H ───────────────────────────────────────────────
|
// ── Form 60 / Form 15G/15H ───────────────────────────────────────────────
|
||||||
@Column(name = "form60_type", length = 20)
|
@Column(name = "form60_type", length = 20)
|
||||||
private String form60Type;
|
private String form60Type;
|
||||||
@@ -240,6 +321,18 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "remarks", length = 500)
|
@Column(name = "remarks", length = 500)
|
||||||
private String remarks;
|
private String remarks;
|
||||||
|
|
||||||
|
@Column(name = "is_exception", nullable = false)
|
||||||
|
private Boolean isException;
|
||||||
|
|
||||||
|
@Column(name = "exception_reason", length = 200)
|
||||||
|
private String exceptionReason;
|
||||||
|
|
||||||
|
@Column(name = "exception_resolved", nullable = false)
|
||||||
|
private Boolean exceptionResolved;
|
||||||
|
|
||||||
|
@Column(name = "exception_resolved_on")
|
||||||
|
private LocalDate exceptionResolvedOn;
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
protected ClientKyc() {}
|
protected ClientKyc() {}
|
||||||
@@ -260,7 +353,9 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
this.gstinVerified = false;
|
this.gstinVerified = false;
|
||||||
this.cinVerified = false;
|
this.cinVerified = false;
|
||||||
this.dinVerified = false;
|
this.dinVerified = false;
|
||||||
this.initiatedOn = LocalDate.now();
|
this.isException = false;
|
||||||
|
this.exceptionResolved = false;
|
||||||
|
this.initiatedOn = DateUtils.getBusinessLocalDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
|
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
|
||||||
@@ -287,6 +382,24 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
private void applyExtendedFields(final JsonCommand command) {
|
private void applyExtendedFields(final JsonCommand command) {
|
||||||
this.panAadhaarSeedingStatus = StringUtils.defaultIfEmpty(
|
this.panAadhaarSeedingStatus = StringUtils.defaultIfEmpty(
|
||||||
command.stringValueOfParameterNamed("panAadhaarSeedingStatus"), this.panAadhaarSeedingStatus);
|
command.stringValueOfParameterNamed("panAadhaarSeedingStatus"), this.panAadhaarSeedingStatus);
|
||||||
|
final Boolean panDeclarationTakenVal = command.booleanObjectValueOfParameterNamed("panDeclarationTaken");
|
||||||
|
this.panDeclarationTaken = panDeclarationTakenVal != null ? panDeclarationTakenVal : false;
|
||||||
|
this.panDeclarationMismatchFields = StringUtils.defaultIfEmpty(
|
||||||
|
command.stringValueOfParameterNamed("panDeclarationMismatchFields"), null);
|
||||||
|
this.panDeclarationNameOnPan = StringUtils.defaultIfEmpty(
|
||||||
|
command.stringValueOfParameterNamed("panDeclarationNameOnPan"), null);
|
||||||
|
this.panDeclarationDobOnPan = command.localDateValueOfParameterNamed("panDeclarationDobOnPan");
|
||||||
|
this.panDeclarationRemarks = StringUtils.defaultIfEmpty(
|
||||||
|
command.stringValueOfParameterNamed("panDeclarationRemarks"), null);
|
||||||
|
this.panDeclarationTakenOn = command.localDateValueOfParameterNamed("panDeclarationTakenOn");
|
||||||
|
final Boolean aadhaarReceiptTakenVal = command.booleanObjectValueOfParameterNamed("aadhaarReceiptTaken");
|
||||||
|
this.aadhaarReceiptTaken = aadhaarReceiptTakenVal != null ? aadhaarReceiptTakenVal : false;
|
||||||
|
this.aadhaarEnrolmentId = StringUtils.defaultIfEmpty(
|
||||||
|
command.stringValueOfParameterNamed("aadhaarEnrolmentId"), null);
|
||||||
|
this.aadhaarEnrolmentDate = command.localDateValueOfParameterNamed("aadhaarEnrolmentDate");
|
||||||
|
this.aadhaarReceiptRemarks = StringUtils.defaultIfEmpty(
|
||||||
|
command.stringValueOfParameterNamed("aadhaarReceiptRemarks"), null);
|
||||||
|
this.aadhaarReceiptTakenOn = command.localDateValueOfParameterNamed("aadhaarReceiptTakenOn");
|
||||||
this.enterprisePan = StringUtils.defaultIfEmpty(
|
this.enterprisePan = StringUtils.defaultIfEmpty(
|
||||||
command.stringValueOfParameterNamed("enterprisePan"), null);
|
command.stringValueOfParameterNamed("enterprisePan"), null);
|
||||||
this.cinNumber = StringUtils.defaultIfEmpty(
|
this.cinNumber = StringUtils.defaultIfEmpty(
|
||||||
@@ -316,6 +429,10 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
command.stringValueOfParameterNamed("freezeReason"), null);
|
command.stringValueOfParameterNamed("freezeReason"), null);
|
||||||
this.freezeAuthority = StringUtils.defaultIfEmpty(
|
this.freezeAuthority = StringUtils.defaultIfEmpty(
|
||||||
command.stringValueOfParameterNamed("freezeAuthority"), null);
|
command.stringValueOfParameterNamed("freezeAuthority"), null);
|
||||||
|
this.freezeDate = command.localDateValueOfParameterNamed("freezeDate");
|
||||||
|
this.freezeAmount = command.bigDecimalValueOfParameterNamed("freezeAmount");
|
||||||
|
this.unfreezeDate = command.localDateValueOfParameterNamed("unfreezeDate");
|
||||||
|
this.noDebitDate = command.localDateValueOfParameterNamed("noDebitDate");
|
||||||
this.form60Type = StringUtils.defaultIfEmpty(
|
this.form60Type = StringUtils.defaultIfEmpty(
|
||||||
command.stringValueOfParameterNamed("form60Type"), null);
|
command.stringValueOfParameterNamed("form60Type"), null);
|
||||||
this.form60Date = command.localDateValueOfParameterNamed("form60Date");
|
this.form60Date = command.localDateValueOfParameterNamed("form60Date");
|
||||||
@@ -324,15 +441,24 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
command.stringValueOfParameterNamed("form15Type"), null);
|
command.stringValueOfParameterNamed("form15Type"), null);
|
||||||
this.form15Fy = StringUtils.defaultIfEmpty(
|
this.form15Fy = StringUtils.defaultIfEmpty(
|
||||||
command.stringValueOfParameterNamed("form15FY"), null);
|
command.stringValueOfParameterNamed("form15FY"), null);
|
||||||
|
Boolean isExceptionVal = command.booleanObjectValueOfParameterNamed("isException");
|
||||||
|
this.isException = isExceptionVal != null ? isExceptionVal : false;
|
||||||
|
this.exceptionReason = StringUtils.defaultIfEmpty(
|
||||||
|
command.stringValueOfParameterNamed("exceptionReason"), null);
|
||||||
|
Boolean exceptionResolvedVal = command.booleanObjectValueOfParameterNamed("exceptionResolved");
|
||||||
|
this.exceptionResolved = exceptionResolvedVal != null ? exceptionResolvedVal : false;
|
||||||
|
this.exceptionResolvedOn = command.localDateValueOfParameterNamed("exceptionResolvedOn");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> update(final JsonCommand command) {
|
public Map<String, Object> update(final JsonCommand command) {
|
||||||
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
||||||
|
this.fieldsOverwrittenByLastUpdate.clear();
|
||||||
|
|
||||||
final String panNumberParam = "panNumber";
|
final String panNumberParam = "panNumber";
|
||||||
if (command.isChangeInStringParameterNamed(panNumberParam, this.panNumber)) {
|
if (command.isChangeInStringParameterNamed(panNumberParam, this.panNumber)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(panNumberParam);
|
final String newValue = command.stringValueOfParameterNamed(panNumberParam);
|
||||||
actualChanges.put(panNumberParam, newValue);
|
actualChanges.put(panNumberParam, newValue);
|
||||||
|
recordOverwrite(panNumberParam, this.panNumber);
|
||||||
this.panNumber = StringUtils.defaultIfEmpty(newValue, null);
|
this.panNumber = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +466,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInStringParameterNamed(aadhaarParam, this.aadhaarNumberMasked)) {
|
if (command.isChangeInStringParameterNamed(aadhaarParam, this.aadhaarNumberMasked)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(aadhaarParam);
|
final String newValue = command.stringValueOfParameterNamed(aadhaarParam);
|
||||||
actualChanges.put(aadhaarParam, newValue);
|
actualChanges.put(aadhaarParam, newValue);
|
||||||
|
recordOverwrite(aadhaarParam, this.aadhaarNumberMasked);
|
||||||
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
|
this.aadhaarNumberMasked = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +474,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInStringParameterNamed(gstinParam, this.gstinNumber)) {
|
if (command.isChangeInStringParameterNamed(gstinParam, this.gstinNumber)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(gstinParam);
|
final String newValue = command.stringValueOfParameterNamed(gstinParam);
|
||||||
actualChanges.put(gstinParam, newValue);
|
actualChanges.put(gstinParam, newValue);
|
||||||
|
recordOverwrite(gstinParam, this.gstinNumber);
|
||||||
this.gstinNumber = StringUtils.defaultIfEmpty(newValue, null);
|
this.gstinNumber = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,9 +482,20 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInIntegerParameterNamed(riskParam, this.riskCategory)) {
|
if (command.isChangeInIntegerParameterNamed(riskParam, this.riskCategory)) {
|
||||||
final Integer newValue = command.integerValueOfParameterNamed(riskParam);
|
final Integer newValue = command.integerValueOfParameterNamed(riskParam);
|
||||||
actualChanges.put(riskParam, newValue);
|
actualChanges.put(riskParam, newValue);
|
||||||
|
recordOverwrite(riskParam, this.riskCategory);
|
||||||
this.riskCategory = newValue;
|
this.riskCategory = newValue;
|
||||||
|
|
||||||
|
// The re-KYC cycle is a property of the risk category, so moving the category moves the
|
||||||
|
// due date with it — otherwise a customer raised to High keeps a Low customer's ten-year
|
||||||
|
// renewal and is never called back in for the two-yearly review the rating requires.
|
||||||
|
final LocalDate newExpiry = recalculateExpiryForRiskCategory();
|
||||||
|
if (newExpiry != null) {
|
||||||
|
actualChanges.put("expiryDate", newExpiry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remarks is the audit justification itself, so a change to it is deliberately not recorded
|
||||||
|
// as an overwrite — requiring remarks in order to write remarks would be circular.
|
||||||
final String remarksParam = "remarks";
|
final String remarksParam = "remarks";
|
||||||
if (command.isChangeInStringParameterNamed(remarksParam, this.remarks)) {
|
if (command.isChangeInStringParameterNamed(remarksParam, this.remarks)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(remarksParam);
|
final String newValue = command.stringValueOfParameterNamed(remarksParam);
|
||||||
@@ -366,6 +505,28 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
|
|
||||||
updateStringField(command, "panAadhaarSeedingStatus", this.panAadhaarSeedingStatus, actualChanges,
|
updateStringField(command, "panAadhaarSeedingStatus", this.panAadhaarSeedingStatus, actualChanges,
|
||||||
v -> this.panAadhaarSeedingStatus = v);
|
v -> this.panAadhaarSeedingStatus = v);
|
||||||
|
updateBooleanField(command, "panDeclarationTaken", this.panDeclarationTaken, actualChanges,
|
||||||
|
v -> this.panDeclarationTaken = v != null ? v : false);
|
||||||
|
updateStringField(command, "panDeclarationMismatchFields", this.panDeclarationMismatchFields, actualChanges,
|
||||||
|
v -> this.panDeclarationMismatchFields = v);
|
||||||
|
updateStringField(command, "panDeclarationNameOnPan", this.panDeclarationNameOnPan, actualChanges,
|
||||||
|
v -> this.panDeclarationNameOnPan = v);
|
||||||
|
updateDateField(command, "panDeclarationDobOnPan", this.panDeclarationDobOnPan, actualChanges,
|
||||||
|
v -> this.panDeclarationDobOnPan = v);
|
||||||
|
updateStringField(command, "panDeclarationRemarks", this.panDeclarationRemarks, actualChanges,
|
||||||
|
v -> this.panDeclarationRemarks = v);
|
||||||
|
updateDateField(command, "panDeclarationTakenOn", this.panDeclarationTakenOn, actualChanges,
|
||||||
|
v -> this.panDeclarationTakenOn = v);
|
||||||
|
updateBooleanField(command, "aadhaarReceiptTaken", this.aadhaarReceiptTaken, actualChanges,
|
||||||
|
v -> this.aadhaarReceiptTaken = v != null ? v : false);
|
||||||
|
updateStringField(command, "aadhaarEnrolmentId", this.aadhaarEnrolmentId, actualChanges,
|
||||||
|
v -> this.aadhaarEnrolmentId = v);
|
||||||
|
updateDateField(command, "aadhaarEnrolmentDate", this.aadhaarEnrolmentDate, actualChanges,
|
||||||
|
v -> this.aadhaarEnrolmentDate = v);
|
||||||
|
updateStringField(command, "aadhaarReceiptRemarks", this.aadhaarReceiptRemarks, actualChanges,
|
||||||
|
v -> this.aadhaarReceiptRemarks = v);
|
||||||
|
updateDateField(command, "aadhaarReceiptTakenOn", this.aadhaarReceiptTakenOn, actualChanges,
|
||||||
|
v -> this.aadhaarReceiptTakenOn = v);
|
||||||
updateStringField(command, "enterprisePan", this.enterprisePan, actualChanges, v -> this.enterprisePan = v);
|
updateStringField(command, "enterprisePan", this.enterprisePan, actualChanges, v -> this.enterprisePan = v);
|
||||||
updateStringField(command, "cinNumber", this.cinNumber, actualChanges, v -> this.cinNumber = v);
|
updateStringField(command, "cinNumber", this.cinNumber, actualChanges, v -> this.cinNumber = v);
|
||||||
updateStringField(command, "dinNumber", this.dinNumber, actualChanges, v -> this.dinNumber = v);
|
updateStringField(command, "dinNumber", this.dinNumber, actualChanges, v -> this.dinNumber = v);
|
||||||
@@ -391,12 +552,34 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
updateStringField(command, "freezeReason", this.freezeReason, actualChanges, v -> this.freezeReason = v);
|
updateStringField(command, "freezeReason", this.freezeReason, actualChanges, v -> this.freezeReason = v);
|
||||||
updateStringField(command, "freezeAuthority", this.freezeAuthority, actualChanges,
|
updateStringField(command, "freezeAuthority", this.freezeAuthority, actualChanges,
|
||||||
v -> this.freezeAuthority = v);
|
v -> this.freezeAuthority = v);
|
||||||
|
updateDateField(command, "freezeDate", this.freezeDate, actualChanges, v -> this.freezeDate = v);
|
||||||
|
updateBigDecimalField(command, "freezeAmount", this.freezeAmount, actualChanges, v -> this.freezeAmount = v);
|
||||||
|
updateDateField(command, "unfreezeDate", this.unfreezeDate, actualChanges, v -> this.unfreezeDate = v);
|
||||||
|
updateDateField(command, "noDebitDate", this.noDebitDate, actualChanges, v -> this.noDebitDate = v);
|
||||||
updateStringField(command, "form60Type", this.form60Type, actualChanges, v -> this.form60Type = v);
|
updateStringField(command, "form60Type", this.form60Type, actualChanges, v -> this.form60Type = v);
|
||||||
updateDateField(command, "form60Date", this.form60Date, actualChanges, v -> this.form60Date = v);
|
updateDateField(command, "form60Date", this.form60Date, actualChanges, v -> this.form60Date = v);
|
||||||
updateBooleanField(command, "form15Applicable", this.form15Applicable, actualChanges,
|
updateBooleanField(command, "form15Applicable", this.form15Applicable, actualChanges,
|
||||||
v -> this.form15Applicable = v);
|
v -> this.form15Applicable = v);
|
||||||
updateStringField(command, "form15Type", this.form15Type, actualChanges, v -> this.form15Type = v);
|
updateStringField(command, "form15Type", this.form15Type, actualChanges, v -> this.form15Type = v);
|
||||||
updateStringField(command, "form15FY", this.form15Fy, actualChanges, v -> this.form15Fy = v);
|
updateStringField(command, "form15FY", this.form15Fy, actualChanges, v -> this.form15Fy = v);
|
||||||
|
// The four exception columns are derived from whether the identity checks have passed, not
|
||||||
|
// data an officer types, so they are updated without recording an overwrite. A verified
|
||||||
|
// record demands remarks for any field it already held (see
|
||||||
|
// getFieldsOverwrittenByLastUpdate), and is_exception is never null once written — so
|
||||||
|
// counting these would make closing a settled exception impossible without a justification
|
||||||
|
// for something nobody chose. That is what left clients verified on the KYC tab while the
|
||||||
|
// exceptions register still listed them as Pending.
|
||||||
|
updateDerivedBooleanField(command, "isException", this.isException, actualChanges,
|
||||||
|
v -> this.isException = v != null ? v : false);
|
||||||
|
updateDerivedStringField(command, "exceptionReason", this.exceptionReason, actualChanges,
|
||||||
|
v -> this.exceptionReason = v);
|
||||||
|
updateDerivedBooleanField(command, "exceptionResolved", this.exceptionResolved, actualChanges,
|
||||||
|
v -> this.exceptionResolved = v != null ? v : false);
|
||||||
|
updateDerivedDateField(command, "exceptionResolvedOn", this.exceptionResolvedOn, actualChanges,
|
||||||
|
v -> this.exceptionResolvedOn = v);
|
||||||
|
updateBooleanField(command, "panVerified", this.panVerified, actualChanges, v -> this.panVerified = v != null ? v : false);
|
||||||
|
updateBooleanField(command, "aadhaarVerified", this.aadhaarVerified, actualChanges, v -> this.aadhaarVerified = v != null ? v : false);
|
||||||
|
updateBooleanField(command, "gstinVerified", this.gstinVerified, actualChanges, v -> this.gstinVerified = v != null ? v : false);
|
||||||
|
|
||||||
return actualChanges;
|
return actualChanges;
|
||||||
}
|
}
|
||||||
@@ -406,6 +589,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInStringParameterNamed(param, current)) {
|
if (command.isChangeInStringParameterNamed(param, current)) {
|
||||||
final String newValue = command.stringValueOfParameterNamed(param);
|
final String newValue = command.stringValueOfParameterNamed(param);
|
||||||
changes.put(param, newValue);
|
changes.put(param, newValue);
|
||||||
|
recordOverwrite(param, current);
|
||||||
setter.accept(StringUtils.defaultIfEmpty(newValue, null));
|
setter.accept(StringUtils.defaultIfEmpty(newValue, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,6 +599,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
if (command.isChangeInLocalDateParameterNamed(param, current)) {
|
if (command.isChangeInLocalDateParameterNamed(param, current)) {
|
||||||
final LocalDate newValue = command.localDateValueOfParameterNamed(param);
|
final LocalDate newValue = command.localDateValueOfParameterNamed(param);
|
||||||
changes.put(param, newValue);
|
changes.put(param, newValue);
|
||||||
|
recordOverwrite(param, current);
|
||||||
setter.accept(newValue);
|
setter.accept(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,11 +610,89 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
final Boolean newValue = command.booleanObjectValueOfParameterNamed(param);
|
final Boolean newValue = command.booleanObjectValueOfParameterNamed(param);
|
||||||
if ((newValue == null && current != null) || (newValue != null && !newValue.equals(current))) {
|
if ((newValue == null && current != null) || (newValue != null && !newValue.equals(current))) {
|
||||||
changes.put(param, newValue);
|
changes.put(param, newValue);
|
||||||
|
recordOverwrite(param, current);
|
||||||
setter.accept(newValue);
|
setter.accept(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateBigDecimalField(final JsonCommand command, final String param, final BigDecimal current,
|
||||||
|
final Map<String, Object> changes, final java.util.function.Consumer<BigDecimal> setter) {
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed(param, current)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(param);
|
||||||
|
changes.put(param, newValue);
|
||||||
|
recordOverwrite(param, current);
|
||||||
|
setter.accept(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notes that {@code param} replaced a value the record already held. A field going from empty to
|
||||||
|
* populated is the record being completed rather than altered, so it is deliberately not
|
||||||
|
* recorded — see {@link #getFieldsOverwrittenByLastUpdate()}.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* Derived-field variants of the update helpers above.
|
||||||
|
*
|
||||||
|
* Identical, minus the recordOverwrite call: these columns are computed from whether the
|
||||||
|
* identity checks have passed rather than entered by an officer, so replacing one is not the
|
||||||
|
* kind of change a verified record demands a justification for. They still report into
|
||||||
|
* `changes`, so the update is persisted and audited like any other.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private void updateDerivedBooleanField(final JsonCommand command, final String param, final Boolean current,
|
||||||
|
final Map<String, Object> changes, final java.util.function.Consumer<Boolean> setter) {
|
||||||
|
if (command.parameterExists(param)) {
|
||||||
|
final Boolean newValue = command.booleanObjectValueOfParameterNamed(param);
|
||||||
|
if ((newValue == null && current != null) || (newValue != null && !newValue.equals(current))) {
|
||||||
|
changes.put(param, newValue);
|
||||||
|
setter.accept(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDerivedStringField(final JsonCommand command, final String param, final String current,
|
||||||
|
final Map<String, Object> changes, final java.util.function.Consumer<String> setter) {
|
||||||
|
if (command.isChangeInStringParameterNamed(param, current)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed(param);
|
||||||
|
changes.put(param, newValue);
|
||||||
|
setter.accept(StringUtils.defaultIfEmpty(newValue, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDerivedDateField(final JsonCommand command, final String param, final LocalDate current,
|
||||||
|
final Map<String, Object> changes, final java.util.function.Consumer<LocalDate> setter) {
|
||||||
|
if (command.isChangeInLocalDateParameterNamed(param, current)) {
|
||||||
|
final LocalDate newValue = command.localDateValueOfParameterNamed(param);
|
||||||
|
changes.put(param, newValue);
|
||||||
|
setter.accept(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordOverwrite(final String param, final Object currentValue) {
|
||||||
|
final boolean wasPopulated = currentValue instanceof String text
|
||||||
|
? StringUtils.isNotBlank(text)
|
||||||
|
: currentValue != null;
|
||||||
|
if (wasPopulated) {
|
||||||
|
this.fieldsOverwrittenByLastUpdate.add(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters whose already-populated value the most recent {@link #update} replaced.
|
||||||
|
*
|
||||||
|
* <p>Distinguishes altering a record from finishing one: a non-individual KYC auto-verifies as
|
||||||
|
* soon as PAN and GSTIN pass, which happens part-way through the create wizard, and the officer
|
||||||
|
* still has to save the KYC document details afterwards. Those fields are empty at that point,
|
||||||
|
* so filling them in leaves this set empty and no audit justification is demanded — while
|
||||||
|
* changing a document number that was already on the verified record does populate it.
|
||||||
|
*
|
||||||
|
* <p>Reset at the start of every {@link #update} and never persisted: it describes one call.
|
||||||
|
*/
|
||||||
|
public Set<String> getFieldsOverwrittenByLastUpdate() {
|
||||||
|
return Collections.unmodifiableSet(this.fieldsOverwrittenByLastUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
/** Called when documents are submitted and 3rd-party verification is initiated. */
|
/** Called when documents are submitted and 3rd-party verification is initiated. */
|
||||||
public void markInProgress() {
|
public void markInProgress() {
|
||||||
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
|
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
|
||||||
@@ -456,13 +719,13 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
public void markCkycUploaded(final String ckycNumber, final String batchId) {
|
public void markCkycUploaded(final String ckycNumber, final String batchId) {
|
||||||
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
||||||
this.ckycBatchId = StringUtils.defaultIfEmpty(batchId, null);
|
this.ckycBatchId = StringUtils.defaultIfEmpty(batchId, null);
|
||||||
this.ckycUploadedOn = LocalDate.now();
|
this.ckycUploadedOn = DateUtils.getBusinessLocalDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called after successful 3rd-party verification of all required documents. */
|
/** Called after successful 3rd-party verification of all required documents. */
|
||||||
public void markVerified(final String ckycNumber) {
|
public void markVerified(final String ckycNumber) {
|
||||||
this.kycStatus = ClientKycStatus.VERIFIED.getValue();
|
this.kycStatus = ClientKycStatus.VERIFIED.getValue();
|
||||||
this.verifiedOn = LocalDate.now();
|
this.verifiedOn = DateUtils.getBusinessLocalDate();
|
||||||
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
||||||
|
|
||||||
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
|
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
|
||||||
@@ -471,17 +734,45 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-dates the re-KYC due date after the risk category moves.
|
||||||
|
*
|
||||||
|
* The cycle length belongs to the category the customer is in <em>now</em> (Low 10, Medium 8,
|
||||||
|
* High 2), and it is counted from the day of the change rather than from the original
|
||||||
|
* verification. A customer moved to High in year two of a Low rating is then due in two years'
|
||||||
|
* time, not immediately — dating it from {@code verifiedOn} would push most of the book into
|
||||||
|
* an overdue queue the moment a re-rating ran.
|
||||||
|
*
|
||||||
|
* <p>Only a verified KYC carries an expiry: an unverified record has nothing to renew, and
|
||||||
|
* {@link #markVerified} sets the date when it is first verified.
|
||||||
|
*
|
||||||
|
* @return the new expiry date, or {@code null} if the record carries no expiry
|
||||||
|
*/
|
||||||
|
private LocalDate recalculateExpiryForRiskCategory() {
|
||||||
|
if (this.verifiedOn == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
|
||||||
|
if (risk.getReKycYears() <= 0) {
|
||||||
|
return this.expiryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.expiryDate = DateUtils.getBusinessLocalDate().plusYears(risk.getReKycYears());
|
||||||
|
return this.expiryDate;
|
||||||
|
}
|
||||||
|
|
||||||
/** Called when verification fails. */
|
/** Called when verification fails. */
|
||||||
public void markRejected(final String remarks) {
|
public void markRejected(final String remarks) {
|
||||||
this.kycStatus = ClientKycStatus.REJECTED.getValue();
|
this.kycStatus = ClientKycStatus.REJECTED.getValue();
|
||||||
this.rejectedOn = LocalDate.now();
|
this.rejectedOn = DateUtils.getBusinessLocalDate();
|
||||||
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
|
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markPanVerified(final String fullName, final String firstName, final String lastName,
|
public void markPanVerified(final String fullName, final String firstName, final String lastName,
|
||||||
final String aadhaarSeedingStatus, final String category, final String status) {
|
final String aadhaarSeedingStatus, final String category, final String status) {
|
||||||
this.panVerified = true;
|
this.panVerified = true;
|
||||||
this.panVerifiedOn = LocalDate.now();
|
this.panVerifiedOn = DateUtils.getBusinessLocalDate();
|
||||||
this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
|
this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
|
||||||
this.panFirstName = StringUtils.defaultIfEmpty(firstName, null);
|
this.panFirstName = StringUtils.defaultIfEmpty(firstName, null);
|
||||||
this.panLastName = StringUtils.defaultIfEmpty(lastName, null);
|
this.panLastName = StringUtils.defaultIfEmpty(lastName, null);
|
||||||
@@ -507,28 +798,28 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
|
|
||||||
public void markAadhaarVerified() {
|
public void markAadhaarVerified() {
|
||||||
this.aadhaarVerified = true;
|
this.aadhaarVerified = true;
|
||||||
this.aadhaarVerifiedOn = LocalDate.now();
|
this.aadhaarVerifiedOn = DateUtils.getBusinessLocalDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markGstinVerified() {
|
public void markGstinVerified() {
|
||||||
this.gstinVerified = true;
|
this.gstinVerified = true;
|
||||||
this.gstinVerifiedOn = LocalDate.now();
|
this.gstinVerifiedOn = DateUtils.getBusinessLocalDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markCinVerified() {
|
public void markCinVerified() {
|
||||||
this.cinVerified = true;
|
this.cinVerified = true;
|
||||||
this.cinVerifiedOn = LocalDate.now();
|
this.cinVerifiedOn = DateUtils.getBusinessLocalDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markDinVerified() {
|
public void markDinVerified() {
|
||||||
this.dinVerified = true;
|
this.dinVerified = true;
|
||||||
this.dinVerifiedOn = LocalDate.now();
|
this.dinVerifiedOn = DateUtils.getBusinessLocalDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markDinVerified(final String dinNumber, final String mcaResponseJson) {
|
public void markDinVerified(final String dinNumber, final String mcaResponseJson) {
|
||||||
this.dinNumber = dinNumber;
|
this.dinNumber = dinNumber;
|
||||||
this.dinVerified = true;
|
this.dinVerified = true;
|
||||||
this.dinVerifiedOn = LocalDate.now();
|
this.dinVerifiedOn = DateUtils.getBusinessLocalDate();
|
||||||
this.isDirector = true;
|
this.isDirector = true;
|
||||||
this.dinMcaResponse = mcaResponseJson;
|
this.dinMcaResponse = mcaResponseJson;
|
||||||
}
|
}
|
||||||
@@ -549,7 +840,17 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
} else if (type != null && type.isNonIndividual()) {
|
} else if (type != null && type.isNonIndividual()) {
|
||||||
allDone = this.panVerified && this.gstinVerified;
|
allDone = this.panVerified && this.gstinVerified;
|
||||||
}
|
}
|
||||||
if (allDone && !getKycStatus().isVerified()) {
|
// Re-stamped even when the record already reads VERIFIED, because that is exactly what a
|
||||||
|
// periodic re-KYC is: an already-verified customer verified afresh.
|
||||||
|
//
|
||||||
|
// Skipping it there left verifiedOn — and with it the expiry date, and the dormancy clock
|
||||||
|
// that now measures from it — frozen at the original onboarding. An officer who re-verified
|
||||||
|
// both PAN and Aadhaar at the counter saw both rows stamped with today's date and the
|
||||||
|
// account still sitting on the inactive report, with nothing to say what was missing.
|
||||||
|
//
|
||||||
|
// Guarded on the verifications rather than the status, so this only fires when the checks
|
||||||
|
// that constitute a verification have actually been done again.
|
||||||
|
if (allDone) {
|
||||||
markVerified(this.ckycNumber);
|
markVerified(this.ckycNumber);
|
||||||
}
|
}
|
||||||
return allDone;
|
return allDone;
|
||||||
@@ -585,6 +886,18 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return riskCategory;
|
return riskCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directly sets the risk category outside the normal command-diff update path. Used by the AML
|
||||||
|
* module to auto-escalate a client's risk category after their 3rd cumulative STR case -
|
||||||
|
* every such change is recorded in {@code m_client_risk_category_history} by the caller.
|
||||||
|
*/
|
||||||
|
public void escalateRiskCategory(Integer newCategory) {
|
||||||
|
this.riskCategory = newCategory;
|
||||||
|
// An auto-escalation moves the rating for the same reason an officer would, so it moves the
|
||||||
|
// re-KYC due date on the same terms — see recalculateExpiryForRiskCategory().
|
||||||
|
recalculateExpiryForRiskCategory();
|
||||||
|
}
|
||||||
|
|
||||||
public String getPanNumber() {
|
public String getPanNumber() {
|
||||||
return panNumber;
|
return panNumber;
|
||||||
}
|
}
|
||||||
@@ -621,6 +934,60 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return panStatus;
|
return panStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getPanDeclarationTaken() {
|
||||||
|
return panDeclarationTaken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Null-safe form for callers deciding whether the PAN exception is waived. */
|
||||||
|
public boolean isPanDeclarationTaken() {
|
||||||
|
return Boolean.TRUE.equals(this.panDeclarationTaken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPanDeclarationMismatchFields() {
|
||||||
|
return panDeclarationMismatchFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPanDeclarationNameOnPan() {
|
||||||
|
return panDeclarationNameOnPan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPanDeclarationDobOnPan() {
|
||||||
|
return panDeclarationDobOnPan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPanDeclarationRemarks() {
|
||||||
|
return panDeclarationRemarks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getPanDeclarationTakenOn() {
|
||||||
|
return panDeclarationTakenOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getAadhaarReceiptTaken() {
|
||||||
|
return aadhaarReceiptTaken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Null-safe form for callers deciding whether the Aadhaar exception is waived. */
|
||||||
|
public boolean isAadhaarReceiptTaken() {
|
||||||
|
return Boolean.TRUE.equals(this.aadhaarReceiptTaken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAadhaarEnrolmentId() {
|
||||||
|
return aadhaarEnrolmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getAadhaarEnrolmentDate() {
|
||||||
|
return aadhaarEnrolmentDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAadhaarReceiptRemarks() {
|
||||||
|
return aadhaarReceiptRemarks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getAadhaarReceiptTakenOn() {
|
||||||
|
return aadhaarReceiptTakenOn;
|
||||||
|
}
|
||||||
|
|
||||||
public String getAadhaarNumberMasked() {
|
public String getAadhaarNumberMasked() {
|
||||||
return aadhaarNumberMasked;
|
return aadhaarNumberMasked;
|
||||||
}
|
}
|
||||||
@@ -785,6 +1152,38 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return form15Fy;
|
return form15Fy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getIsException() {
|
||||||
|
return isException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsException(Boolean isException) {
|
||||||
|
this.isException = isException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExceptionReason() {
|
||||||
|
return exceptionReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExceptionReason(String exceptionReason) {
|
||||||
|
this.exceptionReason = exceptionReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getExceptionResolved() {
|
||||||
|
return exceptionResolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExceptionResolved(Boolean exceptionResolved) {
|
||||||
|
this.exceptionResolved = exceptionResolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getExceptionResolvedOn() {
|
||||||
|
return exceptionResolvedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExceptionResolvedOn(LocalDate exceptionResolvedOn) {
|
||||||
|
this.exceptionResolvedOn = exceptionResolvedOn;
|
||||||
|
}
|
||||||
|
|
||||||
@Column(name = "is_active", nullable = false)
|
@Column(name = "is_active", nullable = false)
|
||||||
private Boolean isActive = Boolean.TRUE;
|
private Boolean isActive = Boolean.TRUE;
|
||||||
|
|
||||||
@@ -836,4 +1235,20 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
public Long getVersion() {
|
public Long getVersion() {
|
||||||
return this.version;
|
return this.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalDate getFreezeDate() {
|
||||||
|
return this.freezeDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getFreezeAmount() {
|
||||||
|
return this.freezeAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getUnfreezeDate() {
|
||||||
|
return this.unfreezeDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getNoDebitDate() {
|
||||||
|
return this.noDebitDate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One customer-outreach attempt against an open KYC exception.
|
||||||
|
*
|
||||||
|
* <p>Append-only by design — there is no setter-driven update path and the API exposes no PUT or
|
||||||
|
* DELETE. A contact log that can be edited after the fact is worthless as evidence, and RBI's KYC
|
||||||
|
* Master Direction expects the bank to be able to show what it told the customer and when.
|
||||||
|
*
|
||||||
|
* <p>Destinations are stored masked ({@code destinationMasked}); the full mobile number / email
|
||||||
|
* lives on {@code m_client} and is resolved server-side at send time, never accepted from the
|
||||||
|
* browser.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_client_kyc_contact_log")
|
||||||
|
public class ClientKycContactLog {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
/** The KYC record the exception belongs to. Nullable — outreach can precede a KYC row. */
|
||||||
|
@Column(name = "kyc_id")
|
||||||
|
private Long kycId;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "channel", length = 20, nullable = false)
|
||||||
|
private KycContactChannel channel;
|
||||||
|
|
||||||
|
@Column(name = "notes", length = 1000, nullable = false)
|
||||||
|
private String notes;
|
||||||
|
|
||||||
|
@Column(name = "officer_name", length = 200)
|
||||||
|
private String officerName;
|
||||||
|
|
||||||
|
@Column(name = "logged_by_user_id")
|
||||||
|
private Long loggedByUserId;
|
||||||
|
|
||||||
|
@Column(name = "exception_reason", length = 200)
|
||||||
|
private String exceptionReason;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "delivery_status", length = 30, nullable = false)
|
||||||
|
private KycContactDeliveryStatus deliveryStatus;
|
||||||
|
|
||||||
|
@Column(name = "destination_masked", length = 120)
|
||||||
|
private String destinationMasked;
|
||||||
|
|
||||||
|
@Column(name = "provider", length = 40)
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
@Column(name = "provider_reference", length = 120)
|
||||||
|
private String providerReference;
|
||||||
|
|
||||||
|
@Column(name = "failure_reason", length = 500)
|
||||||
|
private String failureReason;
|
||||||
|
|
||||||
|
@Column(name = "message_body", length = 2000)
|
||||||
|
private String messageBody;
|
||||||
|
|
||||||
|
@Column(name = "contacted_at", nullable = false)
|
||||||
|
private LocalDateTime contactedAt;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
protected ClientKycContactLog() {
|
||||||
|
// JPA
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientKycContactLog(final Client client, final Long kycId, final KycContactChannel channel, final String notes,
|
||||||
|
final String officerName, final Long loggedByUserId, final String exceptionReason, final LocalDateTime contactedAt) {
|
||||||
|
this.client = client;
|
||||||
|
this.kycId = kycId;
|
||||||
|
this.channel = channel;
|
||||||
|
this.notes = notes;
|
||||||
|
this.officerName = officerName;
|
||||||
|
this.loggedByUserId = loggedByUserId;
|
||||||
|
this.exceptionReason = exceptionReason;
|
||||||
|
this.contactedAt = contactedAt;
|
||||||
|
this.createdAt = contactedAt;
|
||||||
|
this.deliveryStatus = KycContactDeliveryStatus.NOT_APPLICABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stamp the outcome of a platform send onto this log row. Called once, before the row is
|
||||||
|
* persisted — the entity is append-only afterwards.
|
||||||
|
*/
|
||||||
|
public void recordDeliveryOutcome(final KycContactDeliveryStatus status, final String destinationMasked, final String provider,
|
||||||
|
final String providerReference, final String failureReason, final String messageBody) {
|
||||||
|
this.deliveryStatus = status;
|
||||||
|
this.destinationMasked = destinationMasked;
|
||||||
|
this.provider = provider;
|
||||||
|
this.providerReference = providerReference;
|
||||||
|
this.failureReason = truncate(failureReason, 500);
|
||||||
|
this.messageBody = truncate(messageBody, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String truncate(final String value, final int max) {
|
||||||
|
if (value == null || value.length() <= max) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value.substring(0, max - 3) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Client getClient() {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getKycId() {
|
||||||
|
return this.kycId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KycContactChannel getChannel() {
|
||||||
|
return this.channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNotes() {
|
||||||
|
return this.notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOfficerName() {
|
||||||
|
return this.officerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getLoggedByUserId() {
|
||||||
|
return this.loggedByUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExceptionReason() {
|
||||||
|
return this.exceptionReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KycContactDeliveryStatus getDeliveryStatus() {
|
||||||
|
return this.deliveryStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDestinationMasked() {
|
||||||
|
return this.destinationMasked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProvider() {
|
||||||
|
return this.provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProviderReference() {
|
||||||
|
return this.providerReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFailureReason() {
|
||||||
|
return this.failureReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessageBody() {
|
||||||
|
return this.messageBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getContactedAt() {
|
||||||
|
return this.contactedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return this.createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
public interface ClientKycContactLogRepository extends JpaRepository<ClientKycContactLog, Long> {
|
||||||
|
|
||||||
|
List<ClientKycContactLog> findByClientIdOrderByContactedAtAsc(Long clientId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every contact log for the given clients in one round trip, so the exceptions grid can render
|
||||||
|
* "last contacted" for the whole page without an N+1 query per row.
|
||||||
|
*/
|
||||||
|
@Query("SELECT l FROM ClientKycContactLog l WHERE l.client.id IN :clientIds ORDER BY l.contactedAt ASC")
|
||||||
|
List<ClientKycContactLog> findByClientIds(@Param("clientIds") List<Long> clientIds);
|
||||||
|
}
|
||||||
+25
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.client.domain;
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@@ -33,9 +34,33 @@ public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
|
|||||||
|
|
||||||
boolean existsByClientId(Long clientId);
|
boolean existsByClientId(Long clientId);
|
||||||
|
|
||||||
|
List<ClientKyc> findByIsExceptionTrueAndExceptionResolvedFalse();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolved exceptions, keyed on {@code exceptionResolved} alone.
|
||||||
|
*
|
||||||
|
* <p>Deliberately does NOT also require {@code isException = true}. Resolving an exception
|
||||||
|
* clears {@code isException} — that flag means "has an OPEN exception", so leaving it set on a
|
||||||
|
* closed one would be wrong. An earlier version of this query required both flags, which made
|
||||||
|
* every resolved record invisible in both tabs the moment it was resolved.
|
||||||
|
*/
|
||||||
|
List<ClientKyc> findByExceptionResolvedTrue();
|
||||||
|
|
||||||
@Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status")
|
@Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status")
|
||||||
List<ClientKyc> findAllByStatus(@Param("status") Integer status);
|
List<ClientKyc> findAllByStatus(@Param("status") Integer status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KYC records whose periodic re-KYC due date has passed, oldest first.
|
||||||
|
*
|
||||||
|
* <p>Only records that carry an expiry are considered: an expiry is set when the KYC is first
|
||||||
|
* verified, so one without a date has never been verified and has no renewal cycle to lapse.
|
||||||
|
* A rejected record is excluded for the same reason — it is not a lapsed KYC, it is one that
|
||||||
|
* never took effect, and restricting an account over it would punish the wrong thing.
|
||||||
|
*/
|
||||||
|
@Query("SELECT k FROM ClientKyc k WHERE k.expiryDate IS NOT NULL AND k.expiryDate <= :asOn "
|
||||||
|
+ "AND k.kycStatus <> :rejectedStatus ORDER BY k.expiryDate ASC")
|
||||||
|
List<ClientKyc> findLapsedAsOn(@Param("asOn") LocalDate asOn, @Param("rejectedStatus") Integer rejectedStatus);
|
||||||
|
|
||||||
// ── Pre-KYC uniqueness probes ───────────────────────────────────────────
|
// ── Pre-KYC uniqueness probes ───────────────────────────────────────────
|
||||||
// Why: stop a second client from being created with the same PAN / Aadhaar / GSTIN BEFORE the
|
// Why: stop a second client from being created with the same PAN / Aadhaar / GSTIN BEFORE the
|
||||||
// external KYC API is called, so we don't burn vendor calls on duplicate-identity records. PAN
|
// external KYC API is called, so we don't burn vendor calls on duplicate-identity records. PAN
|
||||||
|
|||||||
+7
-6
@@ -55,12 +55,13 @@ public interface ClientRepository extends JpaRepository<Client, Long>, JpaSpecif
|
|||||||
@Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId")
|
@Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId")
|
||||||
void removeImageId(@Param("clientId") Long clientId);
|
void removeImageId(@Param("clientId") Long clientId);
|
||||||
|
|
||||||
/**
|
@Modifying
|
||||||
* Count clients whose cif_number begins with the given prefix. Used by the CIF number generator
|
@Query("UPDATE Client client SET client.signatureId = :signatureId WHERE client.id = :clientId")
|
||||||
* (TTYYYYNNNNNN format) to compute the next sequence within a given (type, year) bucket.
|
void updateByIdAndSignatureId(@Param("clientId") Long clientId, @Param("signatureId") Long signatureId);
|
||||||
*/
|
|
||||||
@Query("SELECT COUNT(c) FROM Client c WHERE c.cifNumber LIKE CONCAT(:prefix, '%')")
|
@Modifying
|
||||||
long countByCifNumberPrefix(@Param("prefix") String prefix);
|
@Query("UPDATE Client client SET client.signatureId = null WHERE client.id = :clientId")
|
||||||
|
void removeSignatureId(@Param("clientId") Long clientId);
|
||||||
|
|
||||||
// ── Pre-save uniqueness probes ───────────────────────────────────────────
|
// ── Pre-save uniqueness probes ───────────────────────────────────────────
|
||||||
// Why: DB has UNIQUE on mobile_no / email_address, but we want to fail fast with a clear
|
// Why: DB has UNIQUE on mobile_no / email_address, but we want to fail fast with a clear
|
||||||
|
|||||||
+245
@@ -0,0 +1,245 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_client_risk_rule")
|
||||||
|
public class ClientRiskRule {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "rule_name", nullable = false, length = 100)
|
||||||
|
private String ruleName;
|
||||||
|
|
||||||
|
@Column(name = "legal_form_id")
|
||||||
|
private Integer legalFormId;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "occupation_category_cv_id")
|
||||||
|
private CodeValue occupationCategory;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "occupation_cv_id")
|
||||||
|
private CodeValue occupation;
|
||||||
|
|
||||||
|
@Column(name = "min_monthly_income", precision = 19, scale = 6)
|
||||||
|
private BigDecimal minMonthlyIncome;
|
||||||
|
|
||||||
|
@Column(name = "max_monthly_income", precision = 19, scale = 6)
|
||||||
|
private BigDecimal maxMonthlyIncome;
|
||||||
|
|
||||||
|
@Column(name = "min_annual_income", precision = 19, scale = 6)
|
||||||
|
private BigDecimal minAnnualIncome;
|
||||||
|
|
||||||
|
@Column(name = "max_annual_income", precision = 19, scale = 6)
|
||||||
|
private BigDecimal maxAnnualIncome;
|
||||||
|
|
||||||
|
@Column(name = "min_annual_turnover", precision = 19, scale = 6)
|
||||||
|
private BigDecimal minAnnualTurnover;
|
||||||
|
|
||||||
|
@Column(name = "max_annual_turnover", precision = 19, scale = 6)
|
||||||
|
private BigDecimal maxAnnualTurnover;
|
||||||
|
|
||||||
|
@Column(name = "risk_category", nullable = false)
|
||||||
|
private Integer riskCategory;
|
||||||
|
|
||||||
|
@Column(name = "reason", nullable = false, length = 500)
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
@Column(name = "aml_check_status", length = 20)
|
||||||
|
private String amlCheckStatus;
|
||||||
|
|
||||||
|
@Column(name = "banned_entity_match", length = 10)
|
||||||
|
private String bannedEntityMatch;
|
||||||
|
|
||||||
|
@Column(name = "willful_defaulter", length = 10)
|
||||||
|
private String willfulDefaulter;
|
||||||
|
|
||||||
|
@Column(name = "ecgc_sal_flag", length = 10)
|
||||||
|
private String ecgcSalFlag;
|
||||||
|
|
||||||
|
@Column(name = "dormant_flag", length = 20)
|
||||||
|
private String dormantFlag;
|
||||||
|
|
||||||
|
@Column(name = "order_position", nullable = false)
|
||||||
|
private Integer orderPosition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which of the two rule families this row belongs to - {@link #KIND_KYC} or {@link #KIND_AML}.
|
||||||
|
*
|
||||||
|
* This table carries both, and they are not interchangeable. A KYC rule is profile-driven: it
|
||||||
|
* reads occupation / income / turnover to categorise a customer at onboarding (RBI Master
|
||||||
|
* Direction on KYC, Section 12). An AML rule is behaviour-driven: it describes a transaction
|
||||||
|
* pattern and may only fire against posted transactions, surfacing in {@code m_aml_alert}
|
||||||
|
* (Section 13).
|
||||||
|
*
|
||||||
|
* The distinction used to be inferred from "has no criteria set", which migration 0448 erased
|
||||||
|
* by writing 0.0 over the NULLs - after which transaction-monitoring rules began matching
|
||||||
|
* ordinary customers at onboarding and pre-flagging them for AML. Recording the kind means a
|
||||||
|
* rule states what it is; nothing has to guess from the shape of its other columns.
|
||||||
|
*/
|
||||||
|
@Column(name = "rule_kind", length = 10)
|
||||||
|
private String ruleKind;
|
||||||
|
|
||||||
|
public static final String KIND_KYC = "KYC";
|
||||||
|
public static final String KIND_AML = "AML";
|
||||||
|
|
||||||
|
protected ClientRiskRule() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a rule authored from the AML Rules Master screen. Occupation and occupation-category
|
||||||
|
* are attached separately by the caller, which owns the CodeValue lookup.
|
||||||
|
*/
|
||||||
|
public static ClientRiskRule create(final String ruleName, final String reason, final Integer riskCategory,
|
||||||
|
final Integer orderPosition, final Integer legalFormId, final BigDecimal minMonthlyIncome,
|
||||||
|
final BigDecimal maxMonthlyIncome, final BigDecimal minAnnualIncome, final BigDecimal maxAnnualIncome,
|
||||||
|
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover, final String amlCheckStatus,
|
||||||
|
final String bannedEntityMatch, final String willfulDefaulter, final String ecgcSalFlag,
|
||||||
|
final String dormantFlag, final String ruleKind) {
|
||||||
|
final ClientRiskRule rule = new ClientRiskRule();
|
||||||
|
rule.ruleKind = ruleKind;
|
||||||
|
rule.ruleName = ruleName;
|
||||||
|
rule.reason = reason;
|
||||||
|
rule.riskCategory = riskCategory;
|
||||||
|
rule.orderPosition = orderPosition;
|
||||||
|
rule.legalFormId = legalFormId;
|
||||||
|
rule.minMonthlyIncome = minMonthlyIncome;
|
||||||
|
rule.maxMonthlyIncome = maxMonthlyIncome;
|
||||||
|
rule.minAnnualIncome = minAnnualIncome;
|
||||||
|
rule.maxAnnualIncome = maxAnnualIncome;
|
||||||
|
rule.minAnnualTurnover = minAnnualTurnover;
|
||||||
|
rule.maxAnnualTurnover = maxAnnualTurnover;
|
||||||
|
rule.amlCheckStatus = amlCheckStatus;
|
||||||
|
rule.bannedEntityMatch = bannedEntityMatch;
|
||||||
|
rule.willfulDefaulter = willfulDefaulter;
|
||||||
|
rule.ecgcSalFlag = ecgcSalFlag;
|
||||||
|
rule.dormantFlag = dormantFlag;
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOccupationCategory(final CodeValue occupationCategory) {
|
||||||
|
this.occupationCategory = occupationCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOccupation(final CodeValue occupation) {
|
||||||
|
this.occupation = occupation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRuleKind() {
|
||||||
|
return ruleKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when this rule may take part in onboarding risk categorisation. Rows predating the
|
||||||
|
* rule_kind column are treated as KYC, matching how they behaved before AML rules were seeded
|
||||||
|
* into the same table.
|
||||||
|
*/
|
||||||
|
public boolean isKycRule() {
|
||||||
|
return !KIND_AML.equalsIgnoreCase(this.ruleKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRuleName() {
|
||||||
|
return ruleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getLegalFormId() {
|
||||||
|
return legalFormId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodeValue getOccupationCategory() {
|
||||||
|
return occupationCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodeValue getOccupation() {
|
||||||
|
return occupation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMinMonthlyIncome() {
|
||||||
|
return minMonthlyIncome;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMaxMonthlyIncome() {
|
||||||
|
return maxMonthlyIncome;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMinAnnualIncome() {
|
||||||
|
return minAnnualIncome;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMaxAnnualIncome() {
|
||||||
|
return maxAnnualIncome;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMinAnnualTurnover() {
|
||||||
|
return minAnnualTurnover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMaxAnnualTurnover() {
|
||||||
|
return maxAnnualTurnover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getRiskCategory() {
|
||||||
|
return riskCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getOrderPosition() {
|
||||||
|
return orderPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAmlCheckStatus() {
|
||||||
|
return amlCheckStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBannedEntityMatch() {
|
||||||
|
return bannedEntityMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWillfulDefaulter() {
|
||||||
|
return willfulDefaulter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEcgcSalFlag() {
|
||||||
|
return ecgcSalFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDormantFlag() {
|
||||||
|
return dormantFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
|
public interface ClientRiskRuleRepository extends JpaRepository<ClientRiskRule, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT r FROM ClientRiskRule r ORDER BY r.orderPosition ASC")
|
||||||
|
List<ClientRiskRule> findAllOrderedByOrderPosition();
|
||||||
|
|
||||||
|
/** Highest position in use, so a new rule can be appended to the end of the catalog. */
|
||||||
|
@Query("SELECT MAX(r.orderPosition) FROM ClientRiskRule r")
|
||||||
|
Integer findMaxOrderPosition();
|
||||||
|
|
||||||
|
boolean existsByRuleNameIgnoreCase(String ruleName);
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel a KYC officer used (or asked the platform to use) to reach a customer about an open
|
||||||
|
* KYC exception.
|
||||||
|
*
|
||||||
|
* <p>The critical distinction is {@link #isPlatformSent()}:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Offline channels</b> ({@code PHONE_CALL}, {@code BRANCH_VISIT}, {@code FIELD_VISIT}) are
|
||||||
|
* recorded after the fact — the officer already did the thing, the platform only writes it
|
||||||
|
* down.</li>
|
||||||
|
* <li><b>Sending channels</b> ({@code SMS}, {@code EMAIL}, {@code WHATSAPP}) cause the platform to
|
||||||
|
* actually transmit a notice to the customer at save time.</li>
|
||||||
|
* </ul>
|
||||||
|
* Mixing these up would let a "logged" row imply the customer was told something they never
|
||||||
|
* received, so every call site branches on this flag rather than on the channel name.
|
||||||
|
*/
|
||||||
|
public enum KycContactChannel {
|
||||||
|
|
||||||
|
PHONE_CALL("Phone Call", false),
|
||||||
|
SMS("SMS", true),
|
||||||
|
EMAIL("Email", true),
|
||||||
|
WHATSAPP("WhatsApp Message", true),
|
||||||
|
BRANCH_VISIT("In-Branch Visit", false),
|
||||||
|
FIELD_VISIT("Agent Field Visit", false);
|
||||||
|
|
||||||
|
private final String displayLabel;
|
||||||
|
private final boolean platformSent;
|
||||||
|
|
||||||
|
KycContactChannel(final String displayLabel, final boolean platformSent) {
|
||||||
|
this.displayLabel = displayLabel;
|
||||||
|
this.platformSent = platformSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayLabel() {
|
||||||
|
return this.displayLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when saving a log of this channel must actually transmit a message to the customer. */
|
||||||
|
public boolean isPlatformSent() {
|
||||||
|
return this.platformSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lenient parse of whatever the UI sent. Accepts the enum name ({@code SMS}) as well as the
|
||||||
|
* display label, and tolerates the descriptive suffixes the dialog uses — e.g.
|
||||||
|
* {@code "SMS (Aadhaar OTP request)"} and {@code "Email (PAN Link request)"} — because those
|
||||||
|
* labels describe the *purpose* of the message, not a different channel.
|
||||||
|
*
|
||||||
|
* @return the matching channel, or {@code null} when nothing matches.
|
||||||
|
*/
|
||||||
|
public static KycContactChannel fromString(final String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String normalised = raw.trim().toUpperCase();
|
||||||
|
for (final KycContactChannel channel : values()) {
|
||||||
|
if (normalised.equals(channel.name()) || normalised.equals(channel.displayLabel.toUpperCase())) {
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prefix match handles the "(…request)" suffixes. Longest label first so WHATSAPP's
|
||||||
|
// "WHATSAPP MESSAGE" is preferred over a shorter accidental prefix.
|
||||||
|
KycContactChannel best = null;
|
||||||
|
for (final KycContactChannel channel : values()) {
|
||||||
|
final String label = channel.displayLabel.toUpperCase();
|
||||||
|
if (normalised.startsWith(label) && (best == null || label.length() > best.displayLabel.length())) {
|
||||||
|
best = channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outcome of a platform-sent KYC contact notice.
|
||||||
|
*
|
||||||
|
* <p>Every state other than {@link #SENT} means the customer was <em>not</em> reached. The officer
|
||||||
|
* is shown the distinction explicitly, because a contact log that silently swallows a gateway
|
||||||
|
* failure would understate the bank's outreach obligation during a KYC audit.
|
||||||
|
*/
|
||||||
|
public enum KycContactDeliveryStatus {
|
||||||
|
|
||||||
|
/** Offline channel — nothing was transmitted and nothing was supposed to be. */
|
||||||
|
NOT_APPLICABLE,
|
||||||
|
|
||||||
|
/** The gateway accepted the message for delivery. */
|
||||||
|
SENT,
|
||||||
|
|
||||||
|
/** The gateway was reached but rejected the message, or the call errored. */
|
||||||
|
FAILED,
|
||||||
|
|
||||||
|
/** The customer has no mobile number / email address on file for this channel. */
|
||||||
|
SKIPPED_NO_DESTINATION,
|
||||||
|
|
||||||
|
/** No gateway is configured for this channel in this deployment. */
|
||||||
|
PROVIDER_NOT_CONFIGURED;
|
||||||
|
|
||||||
|
public boolean isDelivered() {
|
||||||
|
return this == SENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.client.domain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far an account has travelled down the restriction ladder after its periodic re-KYC lapsed.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* RBI's Master Direction on KYC does not treat an overdue re-KYC as grounds to shut a customer out.
|
||||||
|
* The 2021 amendment (DOR.AML.REC.13/14.01.001/2021-22) is explicit that an account is not to be
|
||||||
|
* closed merely because periodic KYC is pending, and that any restriction follows notice and a fair
|
||||||
|
* opportunity to comply. The ladder therefore leaves the account untouched for a grace period,
|
||||||
|
* stops debits before it stops anything else, and never reaches closure on its own — closure stays
|
||||||
|
* a manual decision with its own notice.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The day counts are bank policy rather than a regulatory figure — the Direction requires "adequate
|
||||||
|
* notice" without fixing a number — so they are declared here once and read from here everywhere.
|
||||||
|
*/
|
||||||
|
public enum KycExpiryStage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inside the grace window. Nothing is restricted; the customer is being chased.
|
||||||
|
*/
|
||||||
|
GRACE(0),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debits stopped, credits still accepted. The first actual restriction, and the one that brings
|
||||||
|
* the customer into the branch without putting their incoming money at risk.
|
||||||
|
*/
|
||||||
|
PARTIAL_FREEZE(90),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Both directions restricted, barring the credits that must always be let through.
|
||||||
|
*/
|
||||||
|
INOPERATIVE(180);
|
||||||
|
|
||||||
|
/** Days past the expiry date at which this stage begins. */
|
||||||
|
private final int fromDaysOverdue;
|
||||||
|
|
||||||
|
KycExpiryStage(final int fromDaysOverdue) {
|
||||||
|
this.fromDaysOverdue = fromDaysOverdue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFromDaysOverdue() {
|
||||||
|
return fromDaysOverdue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stage an account sits at, given how far past its re-KYC due date it is.
|
||||||
|
*
|
||||||
|
* @param daysOverdue
|
||||||
|
* days since the expiry date; zero or negative means the KYC has not lapsed
|
||||||
|
* @return the stage, or {@code null} where the KYC is still current and nothing applies
|
||||||
|
*/
|
||||||
|
public static KycExpiryStage fromDaysOverdue(final long daysOverdue) {
|
||||||
|
if (daysOverdue < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (daysOverdue >= INOPERATIVE.fromDaysOverdue) {
|
||||||
|
return INOPERATIVE;
|
||||||
|
}
|
||||||
|
if (daysOverdue >= PARTIAL_FREEZE.fromDaysOverdue) {
|
||||||
|
return PARTIAL_FREEZE;
|
||||||
|
}
|
||||||
|
return GRACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGrace() {
|
||||||
|
return this == GRACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPartialFreeze() {
|
||||||
|
return this == PARTIAL_FREEZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInoperative() {
|
||||||
|
return this == INOPERATIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether debits should be stopped at this stage. */
|
||||||
|
public boolean blocksDebits() {
|
||||||
|
return this == PARTIAL_FREEZE || this == INOPERATIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether ordinary credits should be stopped at this stage. */
|
||||||
|
public boolean blocksCredits() {
|
||||||
|
return this == INOPERATIVE;
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-1
@@ -24,6 +24,7 @@ import jakarta.persistence.Table;
|
|||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity log for each DIP / MNRL mobile number lookup request.
|
* Activity log for each DIP / MNRL mobile number lookup request.
|
||||||
@@ -64,6 +65,29 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
@Column(name = "looked_up_on", nullable = false)
|
@Column(name = "looked_up_on", nullable = false)
|
||||||
private OffsetDateTime lookedUpOn;
|
private OffsetDateTime lookedUpOn;
|
||||||
|
|
||||||
|
// ── Officer decision on a non-clean result ────────────────────────────────
|
||||||
|
// A DIP hit is a fraud-risk signal rather than a legal bar, so onboarding may continue — but
|
||||||
|
// the officer has to say why in writing, and that reasoning is kept with the lookup.
|
||||||
|
|
||||||
|
/** PROCEEDED | REJECTED. Null while no decision is required or none has been taken. */
|
||||||
|
@Column(name = "override_decision", length = 20)
|
||||||
|
private String overrideDecision;
|
||||||
|
|
||||||
|
@Column(name = "override_reason")
|
||||||
|
private String overrideReason;
|
||||||
|
|
||||||
|
@Column(name = "decided_by")
|
||||||
|
private Long decidedBy;
|
||||||
|
|
||||||
|
@Column(name = "decided_on")
|
||||||
|
private OffsetDateTime decidedOn;
|
||||||
|
|
||||||
|
public static final String STATUS_CLEAN = "CLEAN";
|
||||||
|
public static final String STATUS_ERROR = "ERROR";
|
||||||
|
public static final String STATUS_SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE";
|
||||||
|
public static final String DECISION_PROCEEDED = "PROCEEDED";
|
||||||
|
public static final String DECISION_REJECTED = "REJECTED";
|
||||||
|
|
||||||
protected MobileLookupLog() {}
|
protected MobileLookupLog() {}
|
||||||
|
|
||||||
public MobileLookupLog(final String mobileNumber, final String lookupStatus,
|
public MobileLookupLog(final String mobileNumber, final String lookupStatus,
|
||||||
@@ -78,6 +102,29 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
this.rawResponse = rawResponse;
|
this.rawResponse = rawResponse;
|
||||||
this.lookupSource = lookupSource;
|
this.lookupSource = lookupSource;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.lookedUpOn = OffsetDateTime.now();
|
this.lookedUpOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordDecision(final String decision, final String reason, final Long userId) {
|
||||||
|
this.overrideDecision = decision;
|
||||||
|
this.overrideReason = reason;
|
||||||
|
this.decidedBy = userId;
|
||||||
|
this.decidedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the lookup produced something the officer must consciously decide about. */
|
||||||
|
public boolean requiresDecision() {
|
||||||
|
return this.suspected || this.disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A number may be used only when it came back clean, or when an officer explicitly chose to
|
||||||
|
* proceed. An unreachable DoT service is NOT a pass — it means nothing was checked.
|
||||||
|
*/
|
||||||
|
public boolean isUsable() {
|
||||||
|
if (STATUS_CLEAN.equals(this.lookupStatus)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return DECISION_PROCEEDED.equals(this.overrideDecision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
@@ -19,6 +19,8 @@
|
|||||||
package org.apache.fineract.portfolio.mobilelookup.domain;
|
package org.apache.fineract.portfolio.mobilelookup.domain;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
@@ -35,4 +37,29 @@ public interface MobileLookupLogRepository extends JpaRepository<MobileLookupLog
|
|||||||
|
|
||||||
@Query("SELECT l FROM MobileLookupLog l WHERE l.lookupStatus = :status ORDER BY l.lookedUpOn DESC")
|
@Query("SELECT l FROM MobileLookupLog l WHERE l.lookupStatus = :status ORDER BY l.lookedUpOn DESC")
|
||||||
List<MobileLookupLog> findByLookupStatus(@Param("status") String status);
|
List<MobileLookupLog> findByLookupStatus(@Param("status") String status);
|
||||||
|
|
||||||
|
/** Non-clean lookups still waiting for an officer's decision — the DIP review queue. */
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l WHERE (l.suspected = true OR l.disconnected = true) "
|
||||||
|
+ "AND l.overrideDecision IS NULL ORDER BY l.lookedUpOn DESC")
|
||||||
|
List<MobileLookupLog> findPendingDecisions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most recent lookup for a number, used by the server-side gate to confirm the browser's
|
||||||
|
* claim that a number was verified.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Paged rather than a JPQL {@code LIMIT}, which EclipseLink does not accept — the caller
|
||||||
|
* takes the first row.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l WHERE l.mobileNumber = :mobileNumber ORDER BY l.lookedUpOn DESC")
|
||||||
|
List<MobileLookupLog> findLatestByMobileNumber(@Param("mobileNumber") String mobileNumber, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Earlier rejections of the same number, on any client. A number turned away once is a
|
||||||
|
* standing signal: without this the next officer screens it fresh and sees no history.
|
||||||
|
*/
|
||||||
|
@Query("SELECT l FROM MobileLookupLog l WHERE l.mobileNumber = :mobileNumber "
|
||||||
|
+ "AND l.overrideDecision = 'REJECTED' ORDER BY l.decidedOn DESC")
|
||||||
|
List<MobileLookupLog> findRejections(@Param("mobileNumber") String mobileNumber, Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -18,7 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.sanctions.domain;
|
package org.apache.fineract.portfolio.sanctions.domain;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
@@ -40,4 +42,11 @@ public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry,
|
|||||||
void deleteAllByActiveTrue();
|
void deleteAllByActiveTrue();
|
||||||
|
|
||||||
long countByActiveTrue();
|
long countByActiveTrue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp the active sanctions list was last loaded into the database — i.e. the data
|
||||||
|
* currency shown to callers so they know which list snapshot a screening ran against.
|
||||||
|
*/
|
||||||
|
@Query("SELECT MAX(s.createdDate) FROM SanctionsEntry s WHERE s.active = true")
|
||||||
|
Optional<OffsetDateTime> findListLastLoadedOn();
|
||||||
}
|
}
|
||||||
|
|||||||
+141
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity log for each sanctions screening request.
|
* Activity log for each sanctions screening request.
|
||||||
@@ -60,6 +61,65 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
|
|||||||
@Column(name = "screened_on", nullable = false)
|
@Column(name = "screened_on", nullable = false)
|
||||||
private OffsetDateTime screenedOn;
|
private OffsetDateTime screenedOn;
|
||||||
|
|
||||||
|
// ── Alert disposition ─────────────────────────────────────────────────────
|
||||||
|
// A FLAGGED screening opens a case. It stays PENDING_REVIEW until an officer either
|
||||||
|
// documents it as a different person (FALSE_POSITIVE, effective only once the Principal
|
||||||
|
// Officer approves) or confirms it (CONFIRMED_MATCH, which freezes and gets reported).
|
||||||
|
|
||||||
|
@Column(name = "disposition", length = 30)
|
||||||
|
private String disposition;
|
||||||
|
|
||||||
|
@Column(name = "disposition_reason")
|
||||||
|
private String dispositionReason;
|
||||||
|
|
||||||
|
@Column(name = "reviewed_by")
|
||||||
|
private Long reviewedBy;
|
||||||
|
|
||||||
|
@Column(name = "reviewed_on")
|
||||||
|
private OffsetDateTime reviewedOn;
|
||||||
|
|
||||||
|
@Column(name = "approved_by")
|
||||||
|
private Long approvedBy;
|
||||||
|
|
||||||
|
@Column(name = "approved_on")
|
||||||
|
private OffsetDateTime approvedOn;
|
||||||
|
|
||||||
|
@Column(name = "fiu_str_reference", length = 100)
|
||||||
|
private String fiuStrReference;
|
||||||
|
|
||||||
|
@Column(name = "nodal_officer_informed_on")
|
||||||
|
private OffsetDateTime nodalOfficerInformedOn;
|
||||||
|
|
||||||
|
@Column(name = "frozen_account_ids", length = 1000)
|
||||||
|
private String frozenAccountIds;
|
||||||
|
|
||||||
|
@Column(name = "frozen_on")
|
||||||
|
private OffsetDateTime frozenOn;
|
||||||
|
|
||||||
|
// ── STR tracking ──────────────────────────────────────────────────────────
|
||||||
|
// A confirmed match is reportable to FIU-IND. Tracked here rather than as an AML alert: an
|
||||||
|
// AML alert is built around a monetary transaction, whereas this is triggered by who the
|
||||||
|
// customer is, so the two are surfaced as separate streams on the FIU screen.
|
||||||
|
|
||||||
|
/** NULL until approval; then PENDING_FILING → FILED. */
|
||||||
|
@Column(name = "str_status", length = 30)
|
||||||
|
private String strStatus;
|
||||||
|
|
||||||
|
@Column(name = "str_due_date")
|
||||||
|
private java.time.LocalDate strDueDate;
|
||||||
|
|
||||||
|
@Column(name = "str_raised_on")
|
||||||
|
private OffsetDateTime strRaisedOn;
|
||||||
|
|
||||||
|
public static final String STR_PENDING_FILING = "PENDING_FILING";
|
||||||
|
public static final String STR_FILED = "FILED";
|
||||||
|
|
||||||
|
public static final String RESULT_FLAGGED = "FLAGGED";
|
||||||
|
public static final String RESULT_CLEAR = "CLEAR";
|
||||||
|
public static final String DISPOSITION_PENDING = "PENDING_REVIEW";
|
||||||
|
public static final String DISPOSITION_FALSE_POSITIVE = "FALSE_POSITIVE";
|
||||||
|
public static final String DISPOSITION_CONFIRMED = "CONFIRMED_MATCH";
|
||||||
|
|
||||||
protected SanctionsScreeningLog() {}
|
protected SanctionsScreeningLog() {}
|
||||||
|
|
||||||
public SanctionsScreeningLog(final String screenedName, final String screeningResult,
|
public SanctionsScreeningLog(final String screenedName, final String screeningResult,
|
||||||
@@ -73,9 +133,76 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
|
|||||||
this.screeningSource = screeningSource;
|
this.screeningSource = screeningSource;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.ipAddress = ipAddress;
|
this.ipAddress = ipAddress;
|
||||||
this.screenedOn = OffsetDateTime.now();
|
this.screenedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
// A hit opens a case; a clear screening has no case to close.
|
||||||
|
this.disposition = RESULT_FLAGGED.equals(screeningResult) ? DISPOSITION_PENDING : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Alert lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the officer's assessment of the hit. Deliberately does NOT clear the block: a
|
||||||
|
* false positive only takes effect once {@link #approve(Long)} is called by the Principal
|
||||||
|
* Officer, so the person who raised it cannot wave their own match through.
|
||||||
|
*/
|
||||||
|
public void recordDisposition(final String newDisposition, final String reason, final Long userId) {
|
||||||
|
this.disposition = newDisposition;
|
||||||
|
this.dispositionReason = reason;
|
||||||
|
this.reviewedBy = userId;
|
||||||
|
this.reviewedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
// Re-opening a previously approved alert must drop the old approval with it.
|
||||||
|
this.approvedBy = null;
|
||||||
|
this.approvedOn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void approve(final Long approverId) {
|
||||||
|
this.approvedBy = approverId;
|
||||||
|
this.approvedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records the FIU-IND STR reference and the date the nodal officer was informed. */
|
||||||
|
public void recordReporting(final String strReference, final OffsetDateTime nodalOfficerInformedOn) {
|
||||||
|
if (strReference != null) {
|
||||||
|
this.fiuStrReference = strReference;
|
||||||
|
// Recording the FIU-IND acknowledgement is what closes the filing obligation, so the
|
||||||
|
// case stops appearing as outstanding on the FIU screen.
|
||||||
|
if (!strReference.isBlank()) {
|
||||||
|
this.strStatus = STR_FILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nodalOfficerInformedOn != null) {
|
||||||
|
this.nodalOfficerInformedOn = nodalOfficerInformedOn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordFreeze(final String accountIds) {
|
||||||
|
this.frozenAccountIds = accountIds;
|
||||||
|
this.frozenOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the STR obligation on approval of a confirmed match. PMLA allows seven working days
|
||||||
|
* from forming the suspicion; seven calendar days is the stricter reading.
|
||||||
|
*/
|
||||||
|
public void raiseStr() {
|
||||||
|
if (this.strStatus != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.strStatus = STR_PENDING_FILING;
|
||||||
|
this.strRaisedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||||
|
this.strDueDate = DateUtils.getBusinessLocalDate().plusDays(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStrPending() { return STR_PENDING_FILING.equals(this.strStatus); }
|
||||||
|
|
||||||
|
public boolean isFlagged() { return RESULT_FLAGGED.equals(this.screeningResult); }
|
||||||
|
|
||||||
|
public boolean isApprovedFalsePositive() {
|
||||||
|
return DISPOSITION_FALSE_POSITIVE.equals(this.disposition) && this.approvedBy != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfirmedMatch() { return DISPOSITION_CONFIRMED.equals(this.disposition); }
|
||||||
|
|
||||||
// ── Accessors ─────────────────────────────────────────────────────────────
|
// ── Accessors ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public String getScreenedName() { return screenedName; }
|
public String getScreenedName() { return screenedName; }
|
||||||
@@ -88,4 +215,17 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
|
|||||||
public void setClientId(Long clientId) { this.clientId = clientId; }
|
public void setClientId(Long clientId) { this.clientId = clientId; }
|
||||||
public String getIpAddress() { return ipAddress; }
|
public String getIpAddress() { return ipAddress; }
|
||||||
public OffsetDateTime getScreenedOn() { return screenedOn; }
|
public OffsetDateTime getScreenedOn() { return screenedOn; }
|
||||||
|
public String getDisposition() { return disposition; }
|
||||||
|
public String getDispositionReason() { return dispositionReason; }
|
||||||
|
public Long getReviewedBy() { return reviewedBy; }
|
||||||
|
public OffsetDateTime getReviewedOn() { return reviewedOn; }
|
||||||
|
public Long getApprovedBy() { return approvedBy; }
|
||||||
|
public OffsetDateTime getApprovedOn() { return approvedOn; }
|
||||||
|
public String getFiuStrReference() { return fiuStrReference; }
|
||||||
|
public OffsetDateTime getNodalOfficerInformedOn() { return nodalOfficerInformedOn; }
|
||||||
|
public String getFrozenAccountIds() { return frozenAccountIds; }
|
||||||
|
public OffsetDateTime getFrozenOn() { return frozenOn; }
|
||||||
|
public String getStrStatus() { return strStatus; }
|
||||||
|
public java.time.LocalDate getStrDueDate() { return strDueDate; }
|
||||||
|
public OffsetDateTime getStrRaisedOn() { return strRaisedOn; }
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -39,4 +39,32 @@ public interface SanctionsScreeningLogRepository extends JpaRepository<Sanctions
|
|||||||
|
|
||||||
@Query("SELECT l FROM SanctionsScreeningLog l WHERE LOWER(l.screenedName) LIKE LOWER(CONCAT('%', :name, '%')) ORDER BY l.screenedOn DESC")
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE LOWER(l.screenedName) LIKE LOWER(CONCAT('%', :name, '%')) ORDER BY l.screenedOn DESC")
|
||||||
List<SanctionsScreeningLog> findByScreenedNameContaining(@Param("name") String name);
|
List<SanctionsScreeningLog> findByScreenedNameContaining(@Param("name") String name);
|
||||||
|
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.disposition = :disposition ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findByDisposition(@Param("disposition") String disposition);
|
||||||
|
|
||||||
|
/** Every open alert — the Principal Officer's review queue. */
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.disposition IS NOT NULL ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findAllAlerts();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A name is only cleared to onboard once a false positive on that exact name has been
|
||||||
|
* approved by the Principal Officer — the maker's own assessment is not enough.
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT(l) FROM SanctionsScreeningLog l WHERE LOWER(TRIM(l.screenedName)) = LOWER(TRIM(:name)) "
|
||||||
|
+ "AND l.disposition = 'FALSE_POSITIVE' AND l.approvedBy IS NOT NULL")
|
||||||
|
long countApprovedFalsePositives(@Param("name") String name);
|
||||||
|
|
||||||
|
/** Latest alert for a client, used to decide whether their accounts must stay frozen. */
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.clientId = :clientId AND l.screeningResult = 'FLAGGED' "
|
||||||
|
+ "ORDER BY l.screenedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findFlaggedByClientId(@Param("clientId") Long clientId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirmed matches carrying an STR obligation — the sanctions stream on the FIU-IND screen.
|
||||||
|
* Pass null for every STR regardless of whether it has been filed.
|
||||||
|
*/
|
||||||
|
@Query("SELECT l FROM SanctionsScreeningLog l WHERE l.strStatus IS NOT NULL "
|
||||||
|
+ "AND (:status IS NULL OR l.strStatus = :status) ORDER BY l.strDueDate ASC, l.strRaisedOn DESC")
|
||||||
|
List<SanctionsScreeningLog> findStrCases(@Param("status") String status);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -31,7 +31,8 @@ public enum DepositAccountOnClosureType {
|
|||||||
WITHDRAW_DEPOSIT(100, "depositAccountClosureType.withdrawDeposit"), //
|
WITHDRAW_DEPOSIT(100, "depositAccountClosureType.withdrawDeposit"), //
|
||||||
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
TRANSFER_TO_SAVINGS(200, "depositAccountClosureType.transferToSavings"), //
|
||||||
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
REINVEST_PRINCIPAL_AND_INTEREST(300, "depositAccountClosureType.reinvestPrincipalAndInterest"), //
|
||||||
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"); //
|
REINVEST_PRINCIPAL_ONLY(400, "depositAccountClosureType.reinvestPrincipalOnly"), //
|
||||||
|
PARTIAL_REINVEST(500, "depositAccountClosureType.partialReinvest"); //
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -51,6 +52,7 @@ public enum DepositAccountOnClosureType {
|
|||||||
case 200 -> TRANSFER_TO_SAVINGS;
|
case 200 -> TRANSFER_TO_SAVINGS;
|
||||||
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
case 300 -> REINVEST_PRINCIPAL_AND_INTEREST;
|
||||||
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
case 400 -> REINVEST_PRINCIPAL_ONLY;
|
||||||
|
case 500 -> PARTIAL_REINVEST;
|
||||||
default -> INVALID;
|
default -> INVALID;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,10 @@ public enum DepositAccountOnClosureType {
|
|||||||
return this.equals(INVALID);
|
return this.equals(INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPartialReinvest() {
|
||||||
|
return this.equals(PARTIAL_REINVEST);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: do we really need this?!?
|
// TODO: do we really need this?!?
|
||||||
public static Object[] integerValues() {
|
public static Object[] integerValues() {
|
||||||
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
return Arrays.stream(values()).filter(value -> !INVALID.equals(value)).map(value -> value.value).toList().toArray();
|
||||||
|
|||||||
+5
@@ -208,6 +208,7 @@ public final class DepositsApiConstants {
|
|||||||
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
public static final String onAccountClosureIdParamName = "onAccountClosureId";
|
||||||
public static final String transferDescriptionParamName = "transferDescription";
|
public static final String transferDescriptionParamName = "transferDescription";
|
||||||
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
public static final String toSavingsAccountIdParamName = "toSavingsAccountId";
|
||||||
|
public static final String reinvestAmountParamName = "reinvestAmount";
|
||||||
public static final String savingsAccounts = "savingsAccounts";
|
public static final String savingsAccounts = "savingsAccounts";
|
||||||
public static final String maturityInstructionIdParamName = "maturityInstructionId";
|
public static final String maturityInstructionIdParamName = "maturityInstructionId";
|
||||||
public static final String transferToSavingsIdParamName = "transferToSavingsId";
|
public static final String transferToSavingsIdParamName = "transferToSavingsId";
|
||||||
@@ -588,6 +589,10 @@ public final class DepositsApiConstants {
|
|||||||
fixedDepositResponseData.add(SavingsApiConstants.jointHoldersParamName);
|
fixedDepositResponseData.add(SavingsApiConstants.jointHoldersParamName);
|
||||||
fixedDepositResponseData.add(SavingsApiConstants.nominationFacilityParamName);
|
fixedDepositResponseData.add(SavingsApiConstants.nominationFacilityParamName);
|
||||||
fixedDepositResponseData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
fixedDepositResponseData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
||||||
|
// FD Special's engine-computed monthly interest schedule (see FixedDepositAccountsApiResource#retrieveOne)
|
||||||
|
fixedDepositResponseData.add("projectedInterestSchedule");
|
||||||
|
fixedDepositResponseData.add("totalProjectedInterest");
|
||||||
|
fixedDepositResponseData.add("totalValueAtMaturity");
|
||||||
return fixedDepositResponseData;
|
return fixedDepositResponseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -92,6 +92,9 @@ public class SavingsApiConstants {
|
|||||||
public static final String statusParamName = "status";
|
public static final String statusParamName = "status";
|
||||||
public static final String subStatusParamName = "subStatus";
|
public static final String subStatusParamName = "subStatus";
|
||||||
public static final String reasonForBlockParamName = "reasonForBlock";
|
public static final String reasonForBlockParamName = "reasonForBlock";
|
||||||
|
/** "CASH" (default, a manual/staff-placed lien) or "CLEARING" (money still moving through the
|
||||||
|
* clearing process — a not-yet-realized cheque/DD deposit, or a presented cheque not yet settled). */
|
||||||
|
public static final String holdCategoryParamName = "holdCategory";
|
||||||
public static final String clientIdParamName = "clientId";
|
public static final String clientIdParamName = "clientId";
|
||||||
public static final String isRetailAccountParamName = "isRetailAccount";
|
public static final String isRetailAccountParamName = "isRetailAccount";
|
||||||
public static final String autogenerateTransactionIdParamName = "autogenerateTransactionId";
|
public static final String autogenerateTransactionIdParamName = "autogenerateTransactionId";
|
||||||
@@ -226,6 +229,7 @@ public class SavingsApiConstants {
|
|||||||
public static final String endDateParamName = "endDate";
|
public static final String endDateParamName = "endDate";
|
||||||
public static final String introducerRequiredParamName = "introducerRequired";
|
public static final String introducerRequiredParamName = "introducerRequired";
|
||||||
public static final String shareLinkingEnabledParamName = "shareLinkingEnabled";
|
public static final String shareLinkingEnabledParamName = "shareLinkingEnabled";
|
||||||
|
public static final String shareAccountIdParamName = "shareAccountId";
|
||||||
public static final String nominationFacilityParamName = "nominationFacility";
|
public static final String nominationFacilityParamName = "nominationFacility";
|
||||||
public static final String dormancyAlertParamName = "dormancyAlert";
|
public static final String dormancyAlertParamName = "dormancyAlert";
|
||||||
public static final String interestCalculateSelectorParamName = "interestCalculateSelector";
|
public static final String interestCalculateSelectorParamName = "interestCalculateSelector";
|
||||||
@@ -254,7 +258,6 @@ public class SavingsApiConstants {
|
|||||||
public static final String neftFacilityParamName = "neftFacility";
|
public static final String neftFacilityParamName = "neftFacility";
|
||||||
public static final String rtgsFacilityParamName = "rtgsFacility";
|
public static final String rtgsFacilityParamName = "rtgsFacility";
|
||||||
public static final String interBankTotalCapFacilityParamName = "interBankTotalCapFacility";
|
public static final String interBankTotalCapFacilityParamName = "interBankTotalCapFacility";
|
||||||
public static final String withdrawalFacilityParamName = "withdrawalFacility";
|
|
||||||
public static final String depositFacilityKycParamName = "depositFacilityKyc";
|
public static final String depositFacilityKycParamName = "depositFacilityKyc";
|
||||||
public static final String virtualDebitCardParamName = "virtualDebitCard";
|
public static final String virtualDebitCardParamName = "virtualDebitCard";
|
||||||
|
|
||||||
|
|||||||
+17
@@ -157,6 +157,8 @@ public final class SavingsAccountData implements Serializable {
|
|||||||
private LocalDate endDate;
|
private LocalDate endDate;
|
||||||
private Boolean introducerRequired;
|
private Boolean introducerRequired;
|
||||||
private Boolean shareLinkingEnabled;
|
private Boolean shareLinkingEnabled;
|
||||||
|
private Long shareAccountId;
|
||||||
|
private String shareAccountNo;
|
||||||
private Boolean nominationFacility;
|
private Boolean nominationFacility;
|
||||||
private Boolean dormancyAlert;
|
private Boolean dormancyAlert;
|
||||||
private Boolean isDormancyTrackingActiveFromProduct;
|
private Boolean isDormancyTrackingActiveFromProduct;
|
||||||
@@ -217,6 +219,21 @@ public final class SavingsAccountData implements Serializable {
|
|||||||
private BigDecimal interestPosting;
|
private BigDecimal interestPosting;
|
||||||
private BigDecimal overdraftPosting;
|
private BigDecimal overdraftPosting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when this deposit pays its interest out to a linked savings account every period (FD Special).
|
||||||
|
* Such interest never stays in the account, so it must never compound -- every period earns on the
|
||||||
|
* principal alone. Transient because it is only read by the batch interest-posting engine.
|
||||||
|
*/
|
||||||
|
private transient boolean transferInterestToLinkedAccount;
|
||||||
|
|
||||||
|
public void setTransferInterestToLinkedAccount(final boolean transferInterestToLinkedAccount) {
|
||||||
|
this.transferInterestToLinkedAccount = transferInterestToLinkedAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTransferInterestToLinkedAccount() {
|
||||||
|
return this.transferInterestToLinkedAccount;
|
||||||
|
}
|
||||||
|
|
||||||
public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
|
public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
|
||||||
BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum,
|
BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum,
|
||||||
EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum,
|
EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum,
|
||||||
|
|||||||
+10
-1
@@ -256,7 +256,16 @@ public final class PostingPeriod {
|
|||||||
this.daysInYear = daysInYear;
|
this.daysInYear = daysInYear;
|
||||||
this.compoundingPeriods = compoundingPeriods;
|
this.compoundingPeriods = compoundingPeriods;
|
||||||
|
|
||||||
if (isSavingsInterestPostingAtCurrentPeriodEnd) {
|
if (isUserPosting) {
|
||||||
|
// "Post interest as on D" — a closure, or the same action from the account screen. The
|
||||||
|
// period it covers ends at D-1, because interest is earned up to but not including the
|
||||||
|
// day it is paid, and SavingsHelper closes the period there. The transaction itself
|
||||||
|
// still belongs on D: that is the day the member was credited, the day the branch did
|
||||||
|
// the work, and the day the cash is handed over. Dating it to the period end instead
|
||||||
|
// stamps it a day before the account was even closed, and the ledger then disagrees with
|
||||||
|
// the closure request, the voucher and the passbook — all of which carry D.
|
||||||
|
this.dateOfPostingTransaction = periodInterval.endDate().plusDays(1);
|
||||||
|
} else if (isSavingsInterestPostingAtCurrentPeriodEnd) {
|
||||||
this.dateOfPostingTransaction = periodInterval.endDate();
|
this.dateOfPostingTransaction = periodInterval.endDate();
|
||||||
} else {
|
} else {
|
||||||
this.dateOfPostingTransaction = periodInterval.endDate().plusDays(1);
|
this.dateOfPostingTransaction = periodInterval.endDate().plusDays(1);
|
||||||
|
|||||||
+4
@@ -797,6 +797,10 @@ public final class SavingsEnumerations {
|
|||||||
optionData = new EnumOptionData(DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getValue().longValue(),
|
optionData = new EnumOptionData(DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getValue().longValue(),
|
||||||
DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getCode(), "Re-Invest Principal Only");
|
DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY.getCode(), "Re-Invest Principal Only");
|
||||||
break;
|
break;
|
||||||
|
case PARTIAL_REINVEST:
|
||||||
|
optionData = new EnumOptionData(DepositAccountOnClosureType.PARTIAL_REINVEST.getValue().longValue(),
|
||||||
|
DepositAccountOnClosureType.PARTIAL_REINVEST.getCode(), "Partial Re-Invest");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return optionData;
|
return optionData;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-6
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.search.data;
|
package org.apache.fineract.portfolio.search.data;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
import org.apache.fineract.portfolio.search.SearchConstants.SearchSupportedResources;
|
import org.apache.fineract.portfolio.search.SearchConstants.SearchSupportedResources;
|
||||||
|
|
||||||
public class SearchConditions {
|
public class SearchConditions {
|
||||||
@@ -37,17 +38,17 @@ public class SearchConditions {
|
|||||||
this.searchResource = searchResource;
|
this.searchResource = searchResource;
|
||||||
this.exactMatch = exactMatch;
|
this.exactMatch = exactMatch;
|
||||||
this.clientSearch = null == searchResource
|
this.clientSearch = null == searchResource
|
||||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.CLIENTS.name().toLowerCase());
|
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.CLIENTS.name().toLowerCase(Locale.ROOT));
|
||||||
this.groupSearch = null == searchResource
|
this.groupSearch = null == searchResource
|
||||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.GROUPS.name().toLowerCase());
|
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.GROUPS.name().toLowerCase(Locale.ROOT));
|
||||||
this.loanSeach = null == searchResource
|
this.loanSeach = null == searchResource
|
||||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.LOANS.name().toLowerCase());
|
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.LOANS.name().toLowerCase(Locale.ROOT));
|
||||||
this.savingSeach = null == searchResource
|
this.savingSeach = null == searchResource
|
||||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.SAVINGS.name().toLowerCase());
|
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.SAVINGS.name().toLowerCase(Locale.ROOT));
|
||||||
this.shareSeach = null == searchResource
|
this.shareSeach = null == searchResource
|
||||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.SHARES.name().toLowerCase());
|
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.SHARES.name().toLowerCase(Locale.ROOT));
|
||||||
this.clientIdentifierSearch = null == searchResource
|
this.clientIdentifierSearch = null == searchResource
|
||||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.CLIENTIDENTIFIERS.name().toLowerCase());
|
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.CLIENTIDENTIFIERS.name().toLowerCase(Locale.ROOT));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchConditions(final String searchQueryParam, final String searchResource, final Boolean clientSearch,
|
public SearchConditions(final String searchQueryParam, final String searchResource, final Boolean clientSearch,
|
||||||
|
|||||||
+1
-1
@@ -321,6 +321,6 @@ public class SearchUtil {
|
|||||||
|
|
||||||
public String camelToSnake(final String camelStr) {
|
public String camelToSnake(final String camelStr) {
|
||||||
return camelStr == null ? null
|
return camelStr == null ? null
|
||||||
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
|
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.agreement.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A versioned, admin-manageable "user agreement" (bank policy/warning) that every user must accept right after
|
||||||
|
* logging in. Only one row may be {@link #active} at a time; enforced transactionally by the write service, not a
|
||||||
|
* DB constraint.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_user_agreement", uniqueConstraints = { @UniqueConstraint(columnNames = "version", name = "uq_user_agreement_version") })
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class UserAgreement extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "version", nullable = false)
|
||||||
|
private Integer version;
|
||||||
|
|
||||||
|
@Column(name = "title", length = 200, nullable = false)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(name = "body", nullable = false)
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private boolean active;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "created_by", nullable = false)
|
||||||
|
private AppUser createdBy;
|
||||||
|
|
||||||
|
@Column(name = "created_on", nullable = false, updatable = false)
|
||||||
|
private OffsetDateTime createdOn;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
private void prePersist() {
|
||||||
|
if (createdOn == null) {
|
||||||
|
createdOn = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserAgreement create(final Integer version, final String title, final String body, final AppUser createdBy) {
|
||||||
|
final UserAgreement agreement = new UserAgreement();
|
||||||
|
agreement.version = version;
|
||||||
|
agreement.title = title;
|
||||||
|
agreement.body = body;
|
||||||
|
agreement.createdBy = createdBy;
|
||||||
|
agreement.active = true;
|
||||||
|
return agreement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activate() {
|
||||||
|
this.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deactivate() {
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.agreement.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit-proof record of a user accepting a given version of the {@link UserAgreement} right after logging in.
|
||||||
|
* {@link #agreementVersion} is a denormalized snapshot of {@code UserAgreement.version} at acceptance time, so the
|
||||||
|
* acceptance record survives future version bumps unaffected.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_user_agreement_acceptance", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(columnNames = { "appuser_id", "agreement_id" }, name = "uq_user_agreement_acceptance") })
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class UserAgreementAcceptance extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "appuser_id", nullable = false)
|
||||||
|
private AppUser appUser;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "agreement_id", nullable = false)
|
||||||
|
private UserAgreement agreement;
|
||||||
|
|
||||||
|
@Column(name = "agreement_version", nullable = false)
|
||||||
|
private Integer agreementVersion;
|
||||||
|
|
||||||
|
@Column(name = "accepted_on", nullable = false, updatable = false)
|
||||||
|
private OffsetDateTime acceptedOn;
|
||||||
|
|
||||||
|
@Column(name = "ip_address", length = 45)
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
@Column(name = "user_agent", length = 500)
|
||||||
|
private String userAgent;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
private void prePersist() {
|
||||||
|
if (acceptedOn == null) {
|
||||||
|
acceptedOn = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserAgreementAcceptance create(final AppUser appUser, final UserAgreement agreement, final String ipAddress,
|
||||||
|
final String userAgent) {
|
||||||
|
final UserAgreementAcceptance acceptance = new UserAgreementAcceptance();
|
||||||
|
acceptance.appUser = appUser;
|
||||||
|
acceptance.agreement = agreement;
|
||||||
|
acceptance.agreementVersion = agreement.getVersion();
|
||||||
|
acceptance.ipAddress = ipAddress;
|
||||||
|
acceptance.userAgent = userAgent;
|
||||||
|
return acceptance;
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.agreement.domain;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface UserAgreementAcceptanceRepository extends JpaRepository<UserAgreementAcceptance, Long> {
|
||||||
|
|
||||||
|
boolean existsByAppUser_IdAndAgreement_Id(Long appUserId, Long agreementId);
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.useradministration.agreement.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface UserAgreementRepository extends JpaRepository<UserAgreement, Long> {
|
||||||
|
|
||||||
|
Optional<UserAgreement> findByActiveTrue();
|
||||||
|
|
||||||
|
List<UserAgreement> findAllByOrderByVersionDesc();
|
||||||
|
}
|
||||||
+1
-1
@@ -39,7 +39,7 @@ public interface BankUserDscRepository extends JpaRepository<BankUserDsc, Long>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Login gate: returns true if the user has at least one usable DSC mapping that's also not
|
* Login gate: returns true if the user has at least one usable DSC mapping that's also not
|
||||||
* past its notAfter date. The {@code dscExpiryDate >= today} clause prevents 2FA-gating a
|
* past its notAfter date. The {@code dscExpiryDate >= today} clause prevents 2FA-gating a
|
||||||
* user who's enrolled but whose cert silently expired between log-ins — they'd otherwise be
|
* user who's enrolled but whose cert silently expired between log-ins — they'd otherwise be
|
||||||
* stuck in a verification loop with a cert the helper would sign with but the server would
|
* stuck in a verification loop with a cert the helper would sign with but the server would
|
||||||
* accept (the verify side filters too, but the gate is what kicks the user into the DSC flow
|
* accept (the verify side filters too, but the gate is what kicks the user into the DSC flow
|
||||||
|
|||||||
+1
@@ -54,6 +54,7 @@
|
|||||||
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
||||||
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
||||||
|
<class>org.apache.fineract.portfolio.client.domain.ClientKyc</class>
|
||||||
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
<class>org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventConfiguration</class>
|
||||||
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
||||||
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
||||||
|
|||||||
-4
@@ -77,9 +77,6 @@ public class CurrentAccountFacilityConfig extends AbstractPersistableCustom<Long
|
|||||||
@Column(name = "inter_bank_total_cap_facility", nullable = false)
|
@Column(name = "inter_bank_total_cap_facility", nullable = false)
|
||||||
private boolean interBankTotalCapFacility = false;
|
private boolean interBankTotalCapFacility = false;
|
||||||
|
|
||||||
@Column(name = "withdrawal_facility", nullable = false)
|
|
||||||
private boolean withdrawalFacility = false;
|
|
||||||
|
|
||||||
@Column(name = "deposit_facility_kyc", nullable = false)
|
@Column(name = "deposit_facility_kyc", nullable = false)
|
||||||
private boolean depositFacilityKyc = false;
|
private boolean depositFacilityKyc = false;
|
||||||
|
|
||||||
@@ -120,7 +117,6 @@ public class CurrentAccountFacilityConfig extends AbstractPersistableCustom<Long
|
|||||||
if (config.has("neftFacility")) setNeftFacility(config.get("neftFacility").getAsBoolean());
|
if (config.has("neftFacility")) setNeftFacility(config.get("neftFacility").getAsBoolean());
|
||||||
if (config.has("rtgsFacility")) setRtgsFacility(config.get("rtgsFacility").getAsBoolean());
|
if (config.has("rtgsFacility")) setRtgsFacility(config.get("rtgsFacility").getAsBoolean());
|
||||||
if (config.has("interBankTotalCapFacility")) setInterBankTotalCapFacility(config.get("interBankTotalCapFacility").getAsBoolean());
|
if (config.has("interBankTotalCapFacility")) setInterBankTotalCapFacility(config.get("interBankTotalCapFacility").getAsBoolean());
|
||||||
if (config.has("withdrawalFacility")) setWithdrawalFacility(config.get("withdrawalFacility").getAsBoolean());
|
|
||||||
if (config.has("depositFacilityKyc")) setDepositFacilityKyc(config.get("depositFacilityKyc").getAsBoolean());
|
if (config.has("depositFacilityKyc")) setDepositFacilityKyc(config.get("depositFacilityKyc").getAsBoolean());
|
||||||
if (config.has("virtualDebitCard")) setVirtualDebitCard(config.get("virtualDebitCard").getAsBoolean());
|
if (config.has("virtualDebitCard")) setVirtualDebitCard(config.get("virtualDebitCard").getAsBoolean());
|
||||||
if (config.has("internetBanking")) setInternetBanking(config.get("internetBanking").getAsBoolean());
|
if (config.has("internetBanking")) setInternetBanking(config.get("internetBanking").getAsBoolean());
|
||||||
|
|||||||
+4
-6
@@ -29,6 +29,7 @@ import java.time.LocalDateTime;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,9 +136,6 @@ public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
|||||||
@Column(name = "inter_bank_total_cap_facility")
|
@Column(name = "inter_bank_total_cap_facility")
|
||||||
private Boolean interBankTotalCapFacility;
|
private Boolean interBankTotalCapFacility;
|
||||||
|
|
||||||
@Column(name = "withdrawal_facility")
|
|
||||||
private Boolean withdrawalFacility;
|
|
||||||
|
|
||||||
@Column(name = "deposit_facility_kyc")
|
@Column(name = "deposit_facility_kyc")
|
||||||
private Boolean depositFacilityKyc;
|
private Boolean depositFacilityKyc;
|
||||||
|
|
||||||
@@ -452,7 +450,7 @@ public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
|||||||
public CurrentAccountPendingEdit(final CurrentAccount currentAccount, final AppUser maker) {
|
public CurrentAccountPendingEdit(final CurrentAccount currentAccount, final AppUser maker) {
|
||||||
this.currentAccount = currentAccount;
|
this.currentAccount = currentAccount;
|
||||||
this.maker = maker;
|
this.maker = maker;
|
||||||
this.createdOn = LocalDateTime.now();
|
this.createdOn = DateUtils.getLocalDateTimeOfTenant();
|
||||||
this.status = STATUS_PENDING;
|
this.status = STATUS_PENDING;
|
||||||
this.pendingLockKey = lockKeyFor(currentAccount.getId());
|
this.pendingLockKey = lockKeyFor(currentAccount.getId());
|
||||||
}
|
}
|
||||||
@@ -582,14 +580,14 @@ public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
|||||||
public void markApproved(final AppUser checker) {
|
public void markApproved(final AppUser checker) {
|
||||||
this.status = STATUS_APPROVED;
|
this.status = STATUS_APPROVED;
|
||||||
this.checker = checker;
|
this.checker = checker;
|
||||||
this.checkedOn = LocalDateTime.now();
|
this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
|
||||||
this.pendingLockKey = null;
|
this.pendingLockKey = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markRejected(final AppUser checker, final String reason) {
|
public void markRejected(final AppUser checker, final String reason) {
|
||||||
this.status = STATUS_REJECTED;
|
this.status = STATUS_REJECTED;
|
||||||
this.checker = checker;
|
this.checker = checker;
|
||||||
this.checkedOn = LocalDateTime.now();
|
this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
|
||||||
this.rejectReason = reason;
|
this.rejectReason = reason;
|
||||||
this.pendingLockKey = null;
|
this.pendingLockKey = null;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -51,7 +51,9 @@ public class CurrentAccountNumberGenerator {
|
|||||||
if (val > maxVal) {
|
if (val > maxVal) {
|
||||||
maxVal = val;
|
maxVal = val;
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {
|
||||||
|
// non-numeric suffix - skip when computing the max existing sequence
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-2
@@ -54,7 +54,6 @@ import com.google.gson.JsonElement;
|
|||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.MonthDay;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -76,7 +75,6 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
|
|||||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
|
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType;
|
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
|
||||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
|
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsProductAccountingDataValidator;
|
import org.apache.fineract.portfolio.savings.data.SavingsProductAccountingDataValidator;
|
||||||
|
|||||||
-7
@@ -56,18 +56,11 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountT
|
|||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productCodeParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productCodeParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.glAccountIdParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.glAccountIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.effectiveDateParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.effectiveToParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.endDateParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dormancyAlertParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dormancyAlertParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculateSelectorParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.averagePeriodParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoPostParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoPostParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.frequencyFixDayParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lastInterestDateParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enableLockinPeriodParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enableLockinPeriodParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.balanceSwapOptionParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.balanceSwapOptionParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.msmeParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.msmeParamName;
|
||||||
|
|||||||
-1
@@ -378,7 +378,6 @@ public class CurrentProductAssembler {
|
|||||||
config.setNeftFacility(facilityConfigJson.has("neftFacility") ? facilityConfigJson.get("neftFacility").getAsBoolean() : false);
|
config.setNeftFacility(facilityConfigJson.has("neftFacility") ? facilityConfigJson.get("neftFacility").getAsBoolean() : false);
|
||||||
config.setRtgsFacility(facilityConfigJson.has("rtgsFacility") ? facilityConfigJson.get("rtgsFacility").getAsBoolean() : false);
|
config.setRtgsFacility(facilityConfigJson.has("rtgsFacility") ? facilityConfigJson.get("rtgsFacility").getAsBoolean() : false);
|
||||||
config.setInterBankTotalCapFacility(facilityConfigJson.has("interBankTotalCapFacility") ? facilityConfigJson.get("interBankTotalCapFacility").getAsBoolean() : false);
|
config.setInterBankTotalCapFacility(facilityConfigJson.has("interBankTotalCapFacility") ? facilityConfigJson.get("interBankTotalCapFacility").getAsBoolean() : false);
|
||||||
config.setWithdrawalFacility(facilityConfigJson.has("withdrawalFacility") ? facilityConfigJson.get("withdrawalFacility").getAsBoolean() : false);
|
|
||||||
config.setDepositFacilityKyc(facilityConfigJson.has("depositFacilityKyc") ? facilityConfigJson.get("depositFacilityKyc").getAsBoolean() : false);
|
config.setDepositFacilityKyc(facilityConfigJson.has("depositFacilityKyc") ? facilityConfigJson.get("depositFacilityKyc").getAsBoolean() : false);
|
||||||
config.setVirtualDebitCard(facilityConfigJson.has("virtualDebitCard") ? facilityConfigJson.get("virtualDebitCard").getAsBoolean() : false);
|
config.setVirtualDebitCard(facilityConfigJson.has("virtualDebitCard") ? facilityConfigJson.get("virtualDebitCard").getAsBoolean() : false);
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
+2
-1
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
|
|||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "m_current_product_deposit_limit_kyc")
|
@Table(name = "m_current_product_deposit_limit_kyc")
|
||||||
@@ -57,12 +58,12 @@ public class CurrentProductDepositLimitKyc extends AbstractPersistableCustom<Lon
|
|||||||
private LocalDateTime createdDate;
|
private LocalDateTime createdDate;
|
||||||
|
|
||||||
protected CurrentProductDepositLimitKyc() {
|
protected CurrentProductDepositLimitKyc() {
|
||||||
this.createdDate = LocalDateTime.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CurrentProductDepositLimitKyc(CurrentProduct currentProduct) {
|
public CurrentProductDepositLimitKyc(CurrentProduct currentProduct) {
|
||||||
this();
|
this();
|
||||||
this.currentProduct = currentProduct;
|
this.currentProduct = currentProduct;
|
||||||
|
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CurrentProduct getCurrentProduct() { return currentProduct; }
|
public CurrentProduct getCurrentProduct() { return currentProduct; }
|
||||||
|
|||||||
+2
-7
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
|
|||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "m_current_product_facility_config")
|
@Table(name = "m_current_product_facility_config")
|
||||||
@@ -74,9 +75,6 @@ public class CurrentProductFacilityConfig extends AbstractPersistableCustom<Long
|
|||||||
@Column(name = "inter_bank_total_cap_facility")
|
@Column(name = "inter_bank_total_cap_facility")
|
||||||
private Boolean interBankTotalCapFacility = false;
|
private Boolean interBankTotalCapFacility = false;
|
||||||
|
|
||||||
@Column(name = "withdrawal_facility")
|
|
||||||
private Boolean withdrawalFacility = false;
|
|
||||||
|
|
||||||
@Column(name = "deposit_facility_kyc")
|
@Column(name = "deposit_facility_kyc")
|
||||||
private Boolean depositFacilityKyc = false;
|
private Boolean depositFacilityKyc = false;
|
||||||
|
|
||||||
@@ -87,12 +85,12 @@ public class CurrentProductFacilityConfig extends AbstractPersistableCustom<Long
|
|||||||
private LocalDateTime createdDate;
|
private LocalDateTime createdDate;
|
||||||
|
|
||||||
protected CurrentProductFacilityConfig() {
|
protected CurrentProductFacilityConfig() {
|
||||||
this.createdDate = LocalDateTime.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CurrentProductFacilityConfig(CurrentProduct currentProduct) {
|
public CurrentProductFacilityConfig(CurrentProduct currentProduct) {
|
||||||
this();
|
this();
|
||||||
this.currentProduct = currentProduct;
|
this.currentProduct = currentProduct;
|
||||||
|
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CurrentProduct getCurrentProduct() { return currentProduct; }
|
public CurrentProduct getCurrentProduct() { return currentProduct; }
|
||||||
@@ -124,8 +122,6 @@ public class CurrentProductFacilityConfig extends AbstractPersistableCustom<Long
|
|||||||
public void setRtgsFacility(Boolean v) { this.rtgsFacility = v != null ? v : false; }
|
public void setRtgsFacility(Boolean v) { this.rtgsFacility = v != null ? v : false; }
|
||||||
public Boolean getInterBankTotalCapFacility() { return interBankTotalCapFacility; }
|
public Boolean getInterBankTotalCapFacility() { return interBankTotalCapFacility; }
|
||||||
public void setInterBankTotalCapFacility(Boolean v) { this.interBankTotalCapFacility = v != null ? v : false; }
|
public void setInterBankTotalCapFacility(Boolean v) { this.interBankTotalCapFacility = v != null ? v : false; }
|
||||||
public Boolean getWithdrawalFacility() { return withdrawalFacility; }
|
|
||||||
public void setWithdrawalFacility(Boolean v) { this.withdrawalFacility = v != null ? v : false; }
|
|
||||||
public Boolean getDepositFacilityKyc() { return depositFacilityKyc; }
|
public Boolean getDepositFacilityKyc() { return depositFacilityKyc; }
|
||||||
public void setDepositFacilityKyc(Boolean v) { this.depositFacilityKyc = v != null ? v : false; }
|
public void setDepositFacilityKyc(Boolean v) { this.depositFacilityKyc = v != null ? v : false; }
|
||||||
public Boolean getVirtualDebitCard() { return virtualDebitCard; }
|
public Boolean getVirtualDebitCard() { return virtualDebitCard; }
|
||||||
@@ -146,7 +142,6 @@ public class CurrentProductFacilityConfig extends AbstractPersistableCustom<Long
|
|||||||
this.neftFacility = other.getNeftFacility();
|
this.neftFacility = other.getNeftFacility();
|
||||||
this.rtgsFacility = other.getRtgsFacility();
|
this.rtgsFacility = other.getRtgsFacility();
|
||||||
this.interBankTotalCapFacility = other.getInterBankTotalCapFacility();
|
this.interBankTotalCapFacility = other.getInterBankTotalCapFacility();
|
||||||
this.withdrawalFacility = other.getWithdrawalFacility();
|
|
||||||
this.depositFacilityKyc = other.getDepositFacilityKyc();
|
this.depositFacilityKyc = other.getDepositFacilityKyc();
|
||||||
this.virtualDebitCard = other.getVirtualDebitCard();
|
this.virtualDebitCard = other.getVirtualDebitCard();
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user