Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37c961f713 |
@@ -1,68 +0,0 @@
|
|||||||
-- 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,6 +127,14 @@ 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,
|
||||||
@@ -160,5 +168,6 @@ 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,6 +17,7 @@ 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;
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ 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);
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ 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;
|
||||||
|
|
||||||
@@ -71,5 +74,6 @@ 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);
|
||||||
|
|||||||
+9
-20
@@ -45,22 +45,6 @@ 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();
|
||||||
|
|
||||||
@@ -75,8 +59,11 @@ 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()
|
baseDataValidator.reset().parameter(paramNameForFinancialActivity).value(financialActivityId).notNull().isOneOfTheseValues(
|
||||||
.isOneOfTheseValues(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());
|
||||||
|
|
||||||
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();
|
||||||
@@ -99,8 +86,10 @@ 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()
|
baseDataValidator.reset().parameter(paramNameForFinancialActivity).value(financialActivityId).ignoreIfNull().isOneOfTheseValues(
|
||||||
.isOneOfTheseValues(MAPPABLE_FINANCIAL_ACTIVITIES);
|
FinancialActivity.ASSET_TRANSFER.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(),
|
||||||
|
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)) {
|
||||||
|
|||||||
-5
@@ -37,7 +37,6 @@ 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() {
|
||||||
@@ -59,8 +58,6 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -82,8 +79,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-36
@@ -181,14 +181,10 @@ 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();
|
||||||
|
|
||||||
// Transfers In Suspense is not offered by every savings product UI, so it is validated only
|
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
|
||||||
// when the client actually sends it. Omitting it leaves the mapping unset rather than failing.
|
.extractLongNamed(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
|
||||||
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element)) {
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
|
||||||
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
|
.value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero();
|
||||||
.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);
|
||||||
@@ -214,29 +210,20 @@ 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)) {
|
||||||
// Overdraft Control / Income From Interest / Write Off apply to overdraft-capable products
|
final Long overdraftAccount = this.fromApiJsonHelper
|
||||||
// only, so they are validated when supplied rather than demanded from every savings product.
|
.extractLongNamed(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), element);
|
||||||
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(),
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue())
|
||||||
element)) {
|
.value(overdraftAccount).notNull().integerGreaterThanZero();
|
||||||
final Long overdraftAccount = this.fromApiJsonHelper
|
|
||||||
.extractLongNamed(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), element);
|
|
||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue())
|
|
||||||
.value(overdraftAccount).notNull().integerGreaterThanZero();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element)) {
|
final Long incomeFromInterest = this.fromApiJsonHelper
|
||||||
final Long incomeFromInterest = this.fromApiJsonHelper
|
.extractLongNamed(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element);
|
||||||
.extractLongNamed(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), element);
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue()).value(incomeFromInterest)
|
||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue())
|
.notNull().integerGreaterThanZero();
|
||||||
.value(incomeFromInterest).notNull().integerGreaterThanZero();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element)) {
|
final Long writtenOff = this.fromApiJsonHelper.extractLongNamed(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(),
|
||||||
final Long writtenOff = this.fromApiJsonHelper
|
element);
|
||||||
.extractLongNamed(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element);
|
baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenOff).notNull()
|
||||||
baseDataValidator.reset().parameter(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue()).value(writtenOff)
|
.integerGreaterThanZero();
|
||||||
.notNull().integerGreaterThanZero();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -279,15 +266,9 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
|
|||||||
|
|
||||||
if (AccountingValidations.isCashBasedAccounting(accountingRuleType)) {
|
if (AccountingValidations.isCashBasedAccounting(accountingRuleType)) {
|
||||||
|
|
||||||
// Optional, not mandatory like the three below: it names an account for cash physically
|
|
||||||
// received at the counter for a share purchase, which every register in this bank now
|
|
||||||
// books against the account the money actually reached instead. Left absent, a product
|
|
||||||
// simply carries no such mapping, and any code path that still needs one (a genuine
|
|
||||||
// over-the-counter collection) fails with a named "GL not mapped" error rather than
|
|
||||||
// silently assuming the till.
|
|
||||||
final Long shareReferenceId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_REFERENCE.getValue(),
|
final Long shareReferenceId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_REFERENCE.getValue(),
|
||||||
element);
|
element);
|
||||||
baseDataValidator.reset().parameter(SharesProductAccountingParams.SHARES_REFERENCE.getValue()).value(shareReferenceId)
|
baseDataValidator.reset().parameter(SharesProductAccountingParams.SHARES_REFERENCE.getValue()).value(shareReferenceId).notNull()
|
||||||
.integerGreaterThanZero();
|
.integerGreaterThanZero();
|
||||||
|
|
||||||
final Long shareSuspenseId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_SUSPENSE.getValue(),
|
final Long shareSuspenseId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_SUSPENSE.getValue(),
|
||||||
|
|||||||
-2
@@ -38,8 +38,6 @@ public interface ProductToGLAccountMappingReadPlatformService {
|
|||||||
|
|
||||||
Map<String, Object> fetchAccountMappingDetailsForSavingsProduct(Long savingsProductId, Integer accountingType);
|
Map<String, Object> fetchAccountMappingDetailsForSavingsProduct(Long savingsProductId, Integer accountingType);
|
||||||
|
|
||||||
Map<String, Object> fetchAccountMappingDetailsForCurrentProduct(Long currentProductId, Integer accountingType);
|
|
||||||
|
|
||||||
List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForSavingsProduct(Long savingsProductId);
|
List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForSavingsProduct(Long savingsProductId);
|
||||||
|
|
||||||
List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForSavingsProduct(Long savingsProductId);
|
List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForSavingsProduct(Long savingsProductId);
|
||||||
|
|||||||
-18
@@ -202,20 +202,6 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
|||||||
return accountMappingDetails;
|
return accountMappingDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A current product's mappings, which live under their own CURRENT product type, laid out as a savings product's. */
|
|
||||||
@Override
|
|
||||||
public Map<String, Object> fetchAccountMappingDetailsForCurrentProduct(final Long currentProductId, final Integer accountingType) {
|
|
||||||
final List<ProductToGLAccountMapping> mappings = productToGLAccountMappingRepository.findAllRegularMappings(currentProductId,
|
|
||||||
PortfolioProductType.CURRENT.getValue());
|
|
||||||
if (AccountingValidations.isCashBasedAccounting(accountingType)) {
|
|
||||||
return setCashSavingsProductToGLAccountMaps(mappings);
|
|
||||||
}
|
|
||||||
if (AccountingValidations.isAccrualPeriodicBasedAccounting(accountingType)) {
|
|
||||||
return setAccrualPeriodicSavingsProductToGLAccountMaps(mappings);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForLoanProduct(final Long loanProductId) {
|
public List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForLoanProduct(final Long loanProductId) {
|
||||||
return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.LOAN, loanProductId);
|
return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.LOAN, loanProductId);
|
||||||
@@ -440,8 +426,6 @@ 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);
|
||||||
@@ -486,8 +470,6 @@ 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);
|
||||||
|
|||||||
-25
@@ -106,20 +106,6 @@ 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,
|
||||||
@@ -262,15 +248,6 @@ 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);
|
|
||||||
// Interest Payable is editable on cash too, and optional. Interest posting routes
|
|
||||||
// through it on either basis, so a product can name one to carry interest declared
|
|
||||||
// but not yet credited; a product that names none still posts straight to Savings
|
|
||||||
// Control. Created-or-merged because a product configured before this row existed
|
|
||||||
// has no mapping to merge into.
|
|
||||||
createOrmergeSavingsToLiabilityAccountMappingChanges(element, SavingProductAccountingParams.INTEREST_PAYABLE.getValue(),
|
|
||||||
savingsProductId, AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), changes);
|
|
||||||
break;
|
break;
|
||||||
case ACCRUAL_PERIODIC:
|
case ACCRUAL_PERIODIC:
|
||||||
// asset
|
// asset
|
||||||
@@ -331,8 +308,6 @@ 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:
|
||||||
|
|||||||
+3
-35
@@ -27,7 +27,6 @@ import org.apache.fineract.accounting.common.AccountingRuleType;
|
|||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
|
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
@@ -167,9 +166,9 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp
|
|||||||
case NONE:
|
case NONE:
|
||||||
break;
|
break;
|
||||||
case CASH_BASED:
|
case CASH_BASED:
|
||||||
// asset -- the one optional mapping here, so unlike the three below it needs its own
|
// asset
|
||||||
// handling for the case the field is now absent (see mergeOrRemoveSharesReferenceMapping).
|
mergeSharesToAssetAccountMappingChanges(element, SharesProductAccountingParams.SHARES_REFERENCE.getValue(), sharesProductId,
|
||||||
mergeOrRemoveSharesReferenceMapping(element, sharesProductId, changes);
|
CashAccountsForShares.SHARES_REFERENCE.getValue(), CashAccountsForShares.SHARES_REFERENCE.toString(), changes);
|
||||||
|
|
||||||
// income
|
// income
|
||||||
mergeSharesToIncomeAccountMappingChanges(element, SharesProductAccountingParams.INCOME_FROM_FEES.getValue(),
|
mergeSharesToIncomeAccountMappingChanges(element, SharesProductAccountingParams.INCOME_FROM_FEES.getValue(),
|
||||||
@@ -195,35 +194,4 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp
|
|||||||
public void deleteSharesProductToGLAccountMapping(final Long sharesProductId) {
|
public void deleteSharesProductToGLAccountMapping(final Long sharesProductId) {
|
||||||
deleteProductToGLAccountMapping(sharesProductId, PortfolioProductType.SHARES);
|
deleteProductToGLAccountMapping(sharesProductId, PortfolioProductType.SHARES);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the one optional share-product mapping, Shares Reference, on an edit that does not change
|
|
||||||
* the accounting rule.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The other three mappings (Suspense, Income From Fees, Equity) are mandatory, so
|
|
||||||
* {@code mergeProductToAccountMappingChanges} only ever needs to handle "present and possibly
|
|
||||||
* changed" for them -- a null value there cannot happen without failing validation first. Shares
|
|
||||||
* Reference can legitimately be absent, and the shared merge helper (used across loan, savings and
|
|
||||||
* share products alike) only handles "present": it silently leaves an existing mapping row alone
|
|
||||||
* when the incoming value is null, rather than clearing it. Editing this one field out of an
|
|
||||||
* existing product would otherwise leave the product still carrying its old GL underneath, invisible
|
|
||||||
* on the edit form but still resolvable by any code path that looks it up directly.
|
|
||||||
*/
|
|
||||||
private void mergeOrRemoveSharesReferenceMapping(final JsonElement element, final Long sharesProductId,
|
|
||||||
final Map<String, Object> changes) {
|
|
||||||
final Long shareReferenceId = this.fromApiJsonHelper.extractLongNamed(SharesProductAccountingParams.SHARES_REFERENCE.getValue(),
|
|
||||||
element);
|
|
||||||
if (shareReferenceId != null) {
|
|
||||||
mergeSharesToAssetAccountMappingChanges(element, SharesProductAccountingParams.SHARES_REFERENCE.getValue(), sharesProductId,
|
|
||||||
CashAccountsForShares.SHARES_REFERENCE.getValue(), CashAccountsForShares.SHARES_REFERENCE.toString(), changes);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final ProductToGLAccountMapping existing = this.accountMappingRepository.findCoreProductToFinAccountMapping(sharesProductId,
|
|
||||||
PortfolioProductType.SHARES.getValue(), CashAccountsForShares.SHARES_REFERENCE.getValue());
|
|
||||||
if (existing != null) {
|
|
||||||
this.accountMappingRepository.delete(existing);
|
|
||||||
changes.put(SharesProductAccountingParams.SHARES_REFERENCE.getValue(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-43
@@ -269,15 +269,7 @@ 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;
|
||||||
|
|
||||||
@@ -325,9 +317,7 @@ 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;
|
||||||
|
|
||||||
@@ -382,8 +372,7 @@ 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;
|
||||||
|
|
||||||
@@ -421,8 +410,7 @@ 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;
|
||||||
|
|
||||||
@@ -449,34 +437,8 @@ public final class AccountingConstants {
|
|||||||
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), //
|
RESERVE_FOR_DIVIDEND(301, "reserveForDividend", GLAccountType.EQUITY), //
|
||||||
DIVIDEND_FORFEITED_INCOME(400, "dividendForfeitedIncome", GLAccountType.INCOME), //
|
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;
|
||||||
|
|||||||
+1
-10
@@ -137,16 +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()) {
|
||||||
// isApprovedByChecker is only ever true here for a genuine checker approval
|
if (isApprovedByChecker || user.isCheckerSuperUser()) {
|
||||||
// (PortfolioCommandSourceWritePlatformServiceImpl.approveEntry(), which has already run
|
|
||||||
// validateMakerCheckerTransaction() and confirmed the approver is not the maker) or a user
|
|
||||||
// editing their own account details (CommandWrapper.isChangeOfOwnUserDetails() - maker-checker
|
|
||||||
// genuinely does not apply there). On the ORIGINAL submission this is always false and `user`
|
|
||||||
// here is always the maker themselves, so there is deliberately no CHECKER_SUPER_USER (or any
|
|
||||||
// other) bypass at this point: holding CHECKER_SUPER_USER lets a user approve someone ELSE's
|
|
||||||
// request without needing the specific <TASK>_CHECKER permission, it must never let a maker
|
|
||||||
// auto-approve their own submission just by holding it.
|
|
||||||
if (isApprovedByChecker) {
|
|
||||||
commandSource.markAsChecked(user);
|
commandSource.markAsChecked(user);
|
||||||
} else {
|
} else {
|
||||||
if (commandSource.isSanitized()) {
|
if (commandSource.isSanitized()) {
|
||||||
|
|||||||
+69
-141
@@ -664,11 +664,75 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteOffice(final Long officeId) {
|
public CommandWrapperBuilder createAgent() {
|
||||||
this.actionName = "DELETE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "OFFICE";
|
this.entityName = "AGENT";
|
||||||
this.entityId = officeId;
|
this.entityId = null;
|
||||||
this.href = "/offices/" + officeId;
|
this.href = "/dds/agents/template";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateAgent(final Long agentId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "AGENT";
|
||||||
|
this.entityId = agentId;
|
||||||
|
this.href = "/dds/agents/" + agentId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createParty() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "PARTY";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/dds/parties/template";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateParty(final Long partyId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "PARTY";
|
||||||
|
this.entityId = partyId;
|
||||||
|
this.href = "/dds/parties/" + partyId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder closeParty(final Long partyId) {
|
||||||
|
this.actionName = "CLOSE";
|
||||||
|
this.entityName = "PARTY";
|
||||||
|
this.entityId = partyId;
|
||||||
|
this.href = "/dds/parties/" + partyId + "?command=close";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createPartyTransaction() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "PARTYTRANSACTION";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/dds/partytransactions/template";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createCommissionSlabs() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "COMMISSIONSLAB";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/dds/commissionslabs/template";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder generateDdsBatchRun() {
|
||||||
|
this.actionName = "GENERATE";
|
||||||
|
this.entityName = "DDSBATCH";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/dds/batchruns/template";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder postDdsBatchRun(final Long batchRunId) {
|
||||||
|
this.actionName = "POST";
|
||||||
|
this.entityName = "DDSBATCH";
|
||||||
|
this.entityId = batchRunId;
|
||||||
|
this.href = "/dds/batchruns/" + batchRunId;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,14 +820,6 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteFund(final Long fundId) {
|
|
||||||
this.actionName = "DELETE";
|
|
||||||
this.entityName = "FUND";
|
|
||||||
this.entityId = fundId;
|
|
||||||
this.href = "/funds/" + fundId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createReport() {
|
public CommandWrapperBuilder createReport() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "REPORT";
|
this.entityName = "REPORT";
|
||||||
@@ -892,70 +948,6 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createPaymentType() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "PAYMENTTYPE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/paymenttypes/template";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updatePaymentType(final Long paymentTypeId) {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "PAYMENTTYPE";
|
|
||||||
this.entityId = paymentTypeId;
|
|
||||||
this.href = "/paymenttypes/" + paymentTypeId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder deletePaymentType(final Long paymentTypeId) {
|
|
||||||
this.actionName = "DELETE";
|
|
||||||
this.entityName = "PAYMENTTYPE";
|
|
||||||
this.entityId = paymentTypeId;
|
|
||||||
this.href = "/paymenttypes/" + paymentTypeId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createSponsorBank() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "SPONSORBANK";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/sponsorbanks";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updateSponsorBank(final Long sponsorBankId) {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "SPONSORBANK";
|
|
||||||
this.entityId = sponsorBankId;
|
|
||||||
this.href = "/sponsorbanks/" + sponsorBankId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updateSponsorDdGlMapping() {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "SPONSORDDGLMAPPING";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/sponsordd/gl-mapping";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updateInstitutionProfile() {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "INSTITUTIONPROFILE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/institutionprofile";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updateBranchProfile(final Long officeId) {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "BRANCHPROFILE";
|
|
||||||
this.entityId = officeId;
|
|
||||||
this.href = "/branchprofile/" + officeId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createLoanProduct() {
|
public CommandWrapperBuilder createLoanProduct() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "LOANPRODUCT";
|
this.entityName = "LOANPRODUCT";
|
||||||
@@ -2291,14 +2283,6 @@ 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";
|
||||||
@@ -2985,15 +2969,6 @@ 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";
|
||||||
@@ -3200,14 +3175,6 @@ 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";
|
||||||
@@ -3462,13 +3429,6 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder updateCurrency() {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "CURRENCY";
|
|
||||||
this.href = "/currencies";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updateExternalServiceProperties(final String externalServiceName) {
|
public CommandWrapperBuilder updateExternalServiceProperties(final String externalServiceName) {
|
||||||
this.actionName = "UPDATE";
|
this.actionName = "UPDATE";
|
||||||
this.entityName = "EXTERNALSERVICES";
|
this.entityName = "EXTERNALSERVICES";
|
||||||
@@ -4809,36 +4769,4 @@ public class CommandWrapperBuilder {
|
|||||||
this.href = "/loans/" + loanId + "/originators/" + originatorId;
|
this.href = "/loans/" + loanId + "/originators/" + originatorId;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createSurvey() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "SURVEY";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/surveys";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updateSurvey(final Long surveyId) {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "SURVEY";
|
|
||||||
this.entityId = surveyId;
|
|
||||||
this.href = "/surveys/" + surveyId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder activateSurvey(final Long surveyId) {
|
|
||||||
this.actionName = "ACTIVATE";
|
|
||||||
this.entityName = "SURVEY";
|
|
||||||
this.entityId = surveyId;
|
|
||||||
this.href = "/surveys/" + surveyId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder deactivateSurvey(final Long surveyId) {
|
|
||||||
this.actionName = "DEACTIVATE";
|
|
||||||
this.entityName = "SURVEY";
|
|
||||||
this.entityId = surveyId;
|
|
||||||
this.href = "/surveys/" + surveyId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-20
@@ -48,12 +48,6 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
private final CommandProcessingService processAndLogCommandService;
|
private final CommandProcessingService processAndLogCommandService;
|
||||||
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
||||||
// No longer consulted by validateMakerCheckerTransaction() below: the maker-identity rule (a user
|
|
||||||
// can never approve their own submission) is now unconditional and isSameMakerCheckerEnabled() no
|
|
||||||
// longer overrides it. Left injected rather than removed - enable-same-maker-checker is still a
|
|
||||||
// live, independently-configurable global switch (System -> Configurations); deleting this field,
|
|
||||||
// and by extension deciding that switch should stop existing altogether, is a separate decision
|
|
||||||
// from closing this specific self-approval loophole.
|
|
||||||
private final ConfigurationDomainService configurationService;
|
private final ConfigurationDomainService configurationService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -129,20 +123,14 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
AppUser appUser = this.context.authenticatedUser();
|
AppUser appUser = this.context.authenticatedUser();
|
||||||
String permissionCode = commandSource.getPermissionCode();
|
String permissionCode = commandSource.getPermissionCode();
|
||||||
appUser.validateHasCheckerPermissionTo(permissionCode);
|
appUser.validateHasCheckerPermissionTo(permissionCode);
|
||||||
// Maker-identity rule: the user who submitted a command may never be the one who approves
|
if (!configurationService.isSameMakerCheckerEnabled() && !appUser.isCheckerSuperUser()) {
|
||||||
// (or rejects) it - this is a fixed platform rule, not overridable by CHECKER_SUPER_USER (that
|
AppUser maker = commandSource.getMaker();
|
||||||
// permission still lets its holder action a DIFFERENT user's request without needing the
|
if (maker == null) {
|
||||||
// specific <TASK>_CHECKER permission - see validateHasCheckerPermissionTo() above - it just no
|
throw new UnsupportedCommandException(permissionCode, "Maker user is missing.");
|
||||||
// longer lets them action their own) and not overridable by the enable-same-maker-checker
|
}
|
||||||
// global switch (isSameMakerCheckerEnabled() is deliberately no longer consulted here; that
|
if (Objects.equals(appUser.getId(), maker.getId())) {
|
||||||
// configuration is left in place and unused for this decision rather than removed, since
|
throw new UnsupportedCommandException(permissionCode, "Can not be checked by the same user.");
|
||||||
// deleting a still-configurable global setting is a separate decision from this one).
|
}
|
||||||
AppUser maker = commandSource.getMaker();
|
|
||||||
if (maker == null) {
|
|
||||||
throw new UnsupportedCommandException(permissionCode, "Maker user is missing.");
|
|
||||||
}
|
|
||||||
if (Objects.equals(appUser.getId(), maker.getId())) {
|
|
||||||
throw new UnsupportedCommandException(permissionCode, "Can not be checked by the same user.");
|
|
||||||
}
|
}
|
||||||
return commandSource;
|
return commandSource;
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -26,8 +26,6 @@ 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,19 +130,6 @@ 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 + ".";
|
||||||
|
|||||||
-6
@@ -39,10 +39,7 @@ public enum JobName {
|
|||||||
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"), //
|
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"), //
|
||||||
@@ -56,7 +53,6 @@ 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"), //
|
||||||
@@ -72,8 +68,6 @@ public enum JobName {
|
|||||||
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"), //
|
CTR_REPORT_GENERATION("AML Monthly CTR Report Generation"), //
|
||||||
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
|
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
|
||||||
CLIENT_BEHAVIOUR_WINDOW("Client Behaviour Window Computation"), //
|
|
||||||
SAVE_RBI_FORM_A_RETURN("Save RBI Form A Return"), //
|
|
||||||
; //
|
; //
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* 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.organisation.agent.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Collection;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable data object representing a DDS agent, including its collection-limit history and
|
||||||
|
* guarantors. Carries the collection/commission/security-account fields merged in from SubAgent
|
||||||
|
* (2026-08-24) - Agent now does the daily collection work directly.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public final class AgentData implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long clientId;
|
||||||
|
private final String clientName;
|
||||||
|
private final String agentCode;
|
||||||
|
private final String agentNameId;
|
||||||
|
private final String agentAddress1;
|
||||||
|
private final String agentAddress2;
|
||||||
|
private final String agentCity;
|
||||||
|
private final String agentPinCode;
|
||||||
|
private final String agentTelephone;
|
||||||
|
private final String agentSavingAccountType;
|
||||||
|
private final String agentSavingAccountNo;
|
||||||
|
private final LocalDate agentOpeningDate;
|
||||||
|
private final LocalDate agentBirthDate;
|
||||||
|
private final BigDecimal agentCurrentBalance;
|
||||||
|
private final BigDecimal agentOpeningBalance;
|
||||||
|
private final String homeBranch;
|
||||||
|
private final String savingBranch;
|
||||||
|
private final Boolean tdsApplicable;
|
||||||
|
private final BigDecimal tdsPercent;
|
||||||
|
private final BigDecimal currentTds;
|
||||||
|
private final BigDecimal tdsAmount;
|
||||||
|
private final String transferBranch;
|
||||||
|
private final String transferAmountAccountType;
|
||||||
|
private final String transferAccountNo;
|
||||||
|
private final Boolean commissionCalculation;
|
||||||
|
private final BigDecimal limit;
|
||||||
|
private final BigDecimal machineRent;
|
||||||
|
private final String agentSecurityBranch;
|
||||||
|
private final String agentSecurityAccountType;
|
||||||
|
private final String agentSecurityAccountNo;
|
||||||
|
private final BigDecimal commission;
|
||||||
|
private final BigDecimal otherCommission;
|
||||||
|
private final BigDecimal agentDeposit;
|
||||||
|
private final BigDecimal festivalAdvance;
|
||||||
|
private final BigDecimal otherDeduction;
|
||||||
|
private final Collection<AgentLimitData> limits;
|
||||||
|
private final Collection<AgentGuarantorData> guarantors;
|
||||||
|
|
||||||
|
public static AgentData instance(final Long id, final Long clientId, final String clientName, final String agentCode,
|
||||||
|
final String agentNameId, final String agentAddress1, final String agentAddress2, final String agentCity,
|
||||||
|
final String agentPinCode, final String agentTelephone, final String agentSavingAccountType,
|
||||||
|
final String agentSavingAccountNo, final LocalDate agentOpeningDate, final LocalDate agentBirthDate,
|
||||||
|
final BigDecimal agentCurrentBalance, final BigDecimal agentOpeningBalance, final String homeBranch,
|
||||||
|
final String savingBranch, final Boolean tdsApplicable, final BigDecimal tdsPercent, final BigDecimal currentTds,
|
||||||
|
final BigDecimal tdsAmount, final String transferBranch, final String transferAmountAccountType,
|
||||||
|
final String transferAccountNo, final Boolean commissionCalculation, final BigDecimal limit, final BigDecimal machineRent,
|
||||||
|
final String agentSecurityBranch, final String agentSecurityAccountType, final String agentSecurityAccountNo,
|
||||||
|
final BigDecimal commission, final BigDecimal otherCommission, final BigDecimal agentDeposit,
|
||||||
|
final BigDecimal festivalAdvance, final BigDecimal otherDeduction, final Collection<AgentLimitData> limits,
|
||||||
|
final Collection<AgentGuarantorData> guarantors) {
|
||||||
|
return new AgentData(id, clientId, clientName, agentCode, agentNameId, agentAddress1, agentAddress2, agentCity, agentPinCode,
|
||||||
|
agentTelephone, agentSavingAccountType, agentSavingAccountNo, agentOpeningDate, agentBirthDate, agentCurrentBalance,
|
||||||
|
agentOpeningBalance, homeBranch, savingBranch, tdsApplicable, tdsPercent, currentTds, tdsAmount, transferBranch,
|
||||||
|
transferAmountAccountType, transferAccountNo, commissionCalculation, limit, machineRent, agentSecurityBranch,
|
||||||
|
agentSecurityAccountType, agentSecurityAccountNo, commission, otherCommission, agentDeposit, festivalAdvance,
|
||||||
|
otherDeduction, limits, guarantors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentData(final Long id, final Long clientId, final String clientName, final String agentCode, final String agentNameId,
|
||||||
|
final String agentAddress1, final String agentAddress2, final String agentCity, final String agentPinCode,
|
||||||
|
final String agentTelephone, final String agentSavingAccountType, final String agentSavingAccountNo,
|
||||||
|
final LocalDate agentOpeningDate, final LocalDate agentBirthDate, final BigDecimal agentCurrentBalance,
|
||||||
|
final BigDecimal agentOpeningBalance, final String homeBranch, final String savingBranch, final Boolean tdsApplicable,
|
||||||
|
final BigDecimal tdsPercent, final BigDecimal currentTds, final BigDecimal tdsAmount, final String transferBranch,
|
||||||
|
final String transferAmountAccountType, final String transferAccountNo, final Boolean commissionCalculation,
|
||||||
|
final BigDecimal limit, final BigDecimal machineRent, final String agentSecurityBranch,
|
||||||
|
final String agentSecurityAccountType, final String agentSecurityAccountNo, final BigDecimal commission,
|
||||||
|
final BigDecimal otherCommission, final BigDecimal agentDeposit, final BigDecimal festivalAdvance,
|
||||||
|
final BigDecimal otherDeduction, final Collection<AgentLimitData> limits, final Collection<AgentGuarantorData> guarantors) {
|
||||||
|
this.id = id;
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.clientName = clientName;
|
||||||
|
this.agentCode = agentCode;
|
||||||
|
this.agentNameId = agentNameId;
|
||||||
|
this.agentAddress1 = agentAddress1;
|
||||||
|
this.agentAddress2 = agentAddress2;
|
||||||
|
this.agentCity = agentCity;
|
||||||
|
this.agentPinCode = agentPinCode;
|
||||||
|
this.agentTelephone = agentTelephone;
|
||||||
|
this.agentSavingAccountType = agentSavingAccountType;
|
||||||
|
this.agentSavingAccountNo = agentSavingAccountNo;
|
||||||
|
this.agentOpeningDate = agentOpeningDate;
|
||||||
|
this.agentBirthDate = agentBirthDate;
|
||||||
|
this.agentCurrentBalance = agentCurrentBalance;
|
||||||
|
this.agentOpeningBalance = agentOpeningBalance;
|
||||||
|
this.homeBranch = homeBranch;
|
||||||
|
this.savingBranch = savingBranch;
|
||||||
|
this.tdsApplicable = tdsApplicable;
|
||||||
|
this.tdsPercent = tdsPercent;
|
||||||
|
this.currentTds = currentTds;
|
||||||
|
this.tdsAmount = tdsAmount;
|
||||||
|
this.transferBranch = transferBranch;
|
||||||
|
this.transferAmountAccountType = transferAmountAccountType;
|
||||||
|
this.transferAccountNo = transferAccountNo;
|
||||||
|
this.commissionCalculation = commissionCalculation;
|
||||||
|
this.limit = limit;
|
||||||
|
this.machineRent = machineRent;
|
||||||
|
this.agentSecurityBranch = agentSecurityBranch;
|
||||||
|
this.agentSecurityAccountType = agentSecurityAccountType;
|
||||||
|
this.agentSecurityAccountNo = agentSecurityAccountNo;
|
||||||
|
this.commission = commission;
|
||||||
|
this.otherCommission = otherCommission;
|
||||||
|
this.agentDeposit = agentDeposit;
|
||||||
|
this.festivalAdvance = festivalAdvance;
|
||||||
|
this.otherDeduction = otherDeduction;
|
||||||
|
this.limits = limits;
|
||||||
|
this.guarantors = guarantors;
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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.organisation.agent.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable data object representing a single agent guarantor.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public final class AgentGuarantorData implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long clientId;
|
||||||
|
private final String guarantorName;
|
||||||
|
private final String address1;
|
||||||
|
private final String address2;
|
||||||
|
private final String city;
|
||||||
|
private final String pinCode;
|
||||||
|
private final String telephone;
|
||||||
|
|
||||||
|
public static AgentGuarantorData instance(final Long id, final Long clientId, final String guarantorName, final String address1,
|
||||||
|
final String address2, final String city, final String pinCode, final String telephone) {
|
||||||
|
return new AgentGuarantorData(id, clientId, guarantorName, address1, address2, city, pinCode, telephone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentGuarantorData(final Long id, final Long clientId, final String guarantorName, final String address1,
|
||||||
|
final String address2, final String city, final String pinCode, final String telephone) {
|
||||||
|
this.id = id;
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.guarantorName = guarantorName;
|
||||||
|
this.address1 = address1;
|
||||||
|
this.address2 = address2;
|
||||||
|
this.city = city;
|
||||||
|
this.pinCode = pinCode;
|
||||||
|
this.telephone = telephone;
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.organisation.agent.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public final class AgentLimitData implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final LocalDate fromDate;
|
||||||
|
private final LocalDate toDate;
|
||||||
|
private final BigDecimal limit;
|
||||||
|
|
||||||
|
public static AgentLimitData instance(final Long id, final LocalDate fromDate, final LocalDate toDate, final BigDecimal limit) {
|
||||||
|
return new AgentLimitData(id, fromDate, toDate, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentLimitData(final Long id, final LocalDate fromDate, final LocalDate toDate, final BigDecimal limit) {
|
||||||
|
this.id = id;
|
||||||
|
this.fromDate = fromDate;
|
||||||
|
this.toDate = toDate;
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
/**
|
||||||
|
* 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.organisation.agent.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.CascadeType;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OrderBy;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DDS (Daily Deposit Scheme) collection agent. Can represent either a person or, at the top of the
|
||||||
|
* hierarchy, the bank itself acting as its own agent. Holds its own settlement savings account and one
|
||||||
|
* or more guarantors.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_agent", uniqueConstraints = { @UniqueConstraint(columnNames = { "agent_code" }, name = "agent_code_UNIQUE") })
|
||||||
|
public class Agent extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@Column(name = "agent_code", length = 20, nullable = false)
|
||||||
|
private String agentCode;
|
||||||
|
|
||||||
|
@Column(name = "agent_name", length = 100, nullable = false)
|
||||||
|
private String agentName;
|
||||||
|
|
||||||
|
@Column(name = "agent_address1", length = 200, nullable = false)
|
||||||
|
private String agentAddress1;
|
||||||
|
|
||||||
|
@Column(name = "agent_address2", length = 200)
|
||||||
|
private String agentAddress2;
|
||||||
|
|
||||||
|
@Column(name = "agent_city", length = 100, nullable = false)
|
||||||
|
private String agentCity;
|
||||||
|
|
||||||
|
@Column(name = "agent_pincode", length = 20, nullable = false)
|
||||||
|
private String agentPinCode;
|
||||||
|
|
||||||
|
@Column(name = "agent_telephone", length = 20)
|
||||||
|
private String agentTelephone;
|
||||||
|
|
||||||
|
@Column(name = "agent_saving_account_type", length = 100, nullable = false)
|
||||||
|
private String agentSavingAccountType;
|
||||||
|
|
||||||
|
@Column(name = "agent_saving_account_no", length = 50, nullable = false)
|
||||||
|
private String agentSavingAccountNo;
|
||||||
|
|
||||||
|
@Column(name = "agent_opening_date", nullable = false)
|
||||||
|
private LocalDate agentOpeningDate;
|
||||||
|
|
||||||
|
@Column(name = "agent_birth_date", nullable = false)
|
||||||
|
private LocalDate agentBirthDate;
|
||||||
|
|
||||||
|
@Column(name = "agent_current_balance", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal agentCurrentBalance;
|
||||||
|
|
||||||
|
@Column(name = "agent_opening_balance", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal agentOpeningBalance;
|
||||||
|
|
||||||
|
// Fields below were merged in from SubAgent (2026-08-24) - the Agent itself now does the daily
|
||||||
|
// collection work directly; SubAgent no longer exists as a separate tier. Ag.Level / Upper Ag.Level
|
||||||
|
// / Collector (MLM) / CRPB Points / Level Update Date were dropped - they only ever meant anything
|
||||||
|
// for a multi-level hierarchy, which no longer exists here.
|
||||||
|
@Column(name = "home_branch", length = 100, nullable = false)
|
||||||
|
private String homeBranch;
|
||||||
|
|
||||||
|
@Column(name = "saving_branch", length = 100, nullable = false)
|
||||||
|
private String savingBranch;
|
||||||
|
|
||||||
|
@Column(name = "tds_applicable", nullable = false)
|
||||||
|
private boolean tdsApplicable;
|
||||||
|
|
||||||
|
@Column(name = "tds_percent", scale = 6, precision = 19)
|
||||||
|
private BigDecimal tdsPercent;
|
||||||
|
|
||||||
|
@Column(name = "current_tds", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal currentTds;
|
||||||
|
|
||||||
|
@Column(name = "tds_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal tdsAmount;
|
||||||
|
|
||||||
|
@Column(name = "transfer_branch", length = 100)
|
||||||
|
private String transferBranch;
|
||||||
|
|
||||||
|
@Column(name = "transfer_amount_account_type", length = 100)
|
||||||
|
private String transferAmountAccountType;
|
||||||
|
|
||||||
|
@Column(name = "transfer_account_no", length = 50)
|
||||||
|
private String transferAccountNo;
|
||||||
|
|
||||||
|
@Column(name = "collector_commission_calculation", nullable = false)
|
||||||
|
private boolean commissionCalculation;
|
||||||
|
|
||||||
|
@Column(name = "collector_limit_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal limitAmount;
|
||||||
|
|
||||||
|
@Column(name = "machine_rent", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal machineRent;
|
||||||
|
|
||||||
|
@Column(name = "agent_security_branch", length = 100)
|
||||||
|
private String agentSecurityBranch;
|
||||||
|
|
||||||
|
@Column(name = "agent_security_account_type", length = 100)
|
||||||
|
private String agentSecurityAccountType;
|
||||||
|
|
||||||
|
@Column(name = "agent_security_account_no", length = 50)
|
||||||
|
private String agentSecurityAccountNo;
|
||||||
|
|
||||||
|
@Column(name = "commission", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal commission;
|
||||||
|
|
||||||
|
@Column(name = "other_commission", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal otherCommission;
|
||||||
|
|
||||||
|
@Column(name = "agent_deposit", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal agentDeposit;
|
||||||
|
|
||||||
|
@Column(name = "festival_advance", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal festivalAdvance;
|
||||||
|
|
||||||
|
@Column(name = "other_deduction", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal otherDeduction;
|
||||||
|
|
||||||
|
@Column(name = "last_commission_date")
|
||||||
|
private LocalDate lastCommissionDate;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
|
@OrderBy("id")
|
||||||
|
private List<AgentGuarantor> guarantors = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
|
@OrderBy("id")
|
||||||
|
private List<AgentLimit> limits = new ArrayList<>();
|
||||||
|
|
||||||
|
protected Agent() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
private Agent(final Client client, final String agentCode, final String agentName, final String agentAddress1,
|
||||||
|
final String agentAddress2, final String agentCity, final String agentPinCode, final String agentTelephone,
|
||||||
|
final String agentSavingAccountType, final String agentSavingAccountNo, final LocalDate agentOpeningDate,
|
||||||
|
final LocalDate agentBirthDate, final BigDecimal agentOpeningBalance, final JsonCommand command) {
|
||||||
|
this.client = client;
|
||||||
|
this.agentCode = agentCode;
|
||||||
|
this.agentName = agentName;
|
||||||
|
this.agentAddress1 = agentAddress1;
|
||||||
|
this.agentAddress2 = StringUtils.defaultIfEmpty(agentAddress2, null);
|
||||||
|
this.agentCity = agentCity;
|
||||||
|
this.agentPinCode = agentPinCode;
|
||||||
|
this.agentTelephone = StringUtils.defaultIfEmpty(agentTelephone, null);
|
||||||
|
this.agentSavingAccountType = agentSavingAccountType;
|
||||||
|
this.agentSavingAccountNo = agentSavingAccountNo;
|
||||||
|
this.agentOpeningDate = agentOpeningDate;
|
||||||
|
this.agentBirthDate = agentBirthDate;
|
||||||
|
this.agentOpeningBalance = agentOpeningBalance == null ? BigDecimal.ZERO : agentOpeningBalance;
|
||||||
|
this.agentCurrentBalance = this.agentOpeningBalance;
|
||||||
|
|
||||||
|
this.homeBranch = command.stringValueOfParameterNamed("homeBranch");
|
||||||
|
this.savingBranch = command.stringValueOfParameterNamed("savingBranch");
|
||||||
|
this.tdsApplicable = command.booleanPrimitiveValueOfParameterNamed("tdsApplicable");
|
||||||
|
this.tdsPercent = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("tdsPercent");
|
||||||
|
this.currentTds = BigDecimal.ZERO;
|
||||||
|
this.tdsAmount = BigDecimal.ZERO;
|
||||||
|
this.transferBranch = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferBranch"), null);
|
||||||
|
this.transferAmountAccountType = StringUtils
|
||||||
|
.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferAmountAccountType"), null);
|
||||||
|
this.transferAccountNo = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("transferAccountNo"), null);
|
||||||
|
this.commissionCalculation = command.booleanPrimitiveValueOfParameterNamed("commissionCalculation");
|
||||||
|
this.limitAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("limit"));
|
||||||
|
this.machineRent = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("machineRent"));
|
||||||
|
this.agentSecurityBranch = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityBranch"),
|
||||||
|
null);
|
||||||
|
this.agentSecurityAccountType = StringUtils
|
||||||
|
.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityAccountType"), null);
|
||||||
|
this.agentSecurityAccountNo = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("agentSecurityAccountNo"),
|
||||||
|
null);
|
||||||
|
this.commission = BigDecimal.ZERO;
|
||||||
|
this.otherCommission = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("otherCommission"));
|
||||||
|
this.agentDeposit = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("agentDeposit"));
|
||||||
|
this.festivalAdvance = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("festivalAdvance"));
|
||||||
|
this.otherDeduction = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("otherDeduction"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Agent fromJson(final Client client, final String agentCode, final JsonCommand command) {
|
||||||
|
final String agentName = command.stringValueOfParameterNamed("agentNameId");
|
||||||
|
final String agentAddress1 = command.stringValueOfParameterNamed("agentAddress1");
|
||||||
|
final String agentAddress2 = command.stringValueOfParameterNamedAllowingNull("agentAddress2");
|
||||||
|
final String agentCity = command.stringValueOfParameterNamed("agentCity");
|
||||||
|
final String agentPinCode = command.stringValueOfParameterNamed("agentPinCode");
|
||||||
|
final String agentTelephone = command.stringValueOfParameterNamedAllowingNull("agentTelephone");
|
||||||
|
final String agentSavingAccountType = command.stringValueOfParameterNamed("agentSavingAccountType");
|
||||||
|
final String agentSavingAccountNo = command.stringValueOfParameterNamed("agentSavingAccountNo");
|
||||||
|
final LocalDate agentOpeningDate = command.localDateValueOfParameterNamed("agentOpeningDate");
|
||||||
|
final LocalDate agentBirthDate = command.localDateValueOfParameterNamed("agentBirthDate");
|
||||||
|
final BigDecimal agentOpeningBalance = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("agentOpeningBalance");
|
||||||
|
|
||||||
|
return new Agent(client, agentCode, agentName, agentAddress1, agentAddress2, agentCity, agentPinCode, agentTelephone,
|
||||||
|
agentSavingAccountType, agentSavingAccountNo, agentOpeningDate, agentBirthDate, agentOpeningBalance, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal orZero(final BigDecimal value) {
|
||||||
|
return value == null ? BigDecimal.ZERO : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addGuarantor(final AgentGuarantor guarantor) {
|
||||||
|
guarantor.setAgent(this);
|
||||||
|
this.guarantors.add(guarantor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetGuarantors() {
|
||||||
|
this.guarantors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addLimit(final AgentLimit limit) {
|
||||||
|
limit.setAgent(this);
|
||||||
|
this.limits.add(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetLimits() {
|
||||||
|
this.limits.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a Commission Posting's amount to this agent's running commission total and stamps the date
|
||||||
|
* commission was last calculated through, so the next Commission Calculation batch only covers
|
||||||
|
* collections since then.
|
||||||
|
*/
|
||||||
|
public void postCommission(final BigDecimal amount, final LocalDate throughDate) {
|
||||||
|
this.commission = this.commission.add(amount);
|
||||||
|
this.lastCommissionDate = throughDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cash this agent has collected from parties but not yet handed over to a branch (an Agent Tran
|
||||||
|
* settlement). Raised by a Party Tran collection, lowered by an Agent Tran settlement - this is the
|
||||||
|
* figure an Agent's collection limit is enforced against.
|
||||||
|
*/
|
||||||
|
public BigDecimal credit(final BigDecimal amount) {
|
||||||
|
this.agentCurrentBalance = this.agentCurrentBalance.add(amount);
|
||||||
|
return this.agentCurrentBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal settle(final BigDecimal amount) {
|
||||||
|
this.agentCurrentBalance = this.agentCurrentBalance.subtract(amount);
|
||||||
|
return this.agentCurrentBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeClient(final Client newClient) {
|
||||||
|
this.client = newClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long clientId() {
|
||||||
|
return this.client.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> update(final JsonCommand command) {
|
||||||
|
|
||||||
|
final Map<String, Object> actualChanges = new LinkedHashMap<>(10);
|
||||||
|
|
||||||
|
if (command.isChangeInLongParameterNamed("clientId", this.client.getId())) {
|
||||||
|
actualChanges.put("clientId", command.longValueOfParameterNamed("clientId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed("agentNameId", this.agentName)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed("agentNameId");
|
||||||
|
actualChanges.put("agentNameId", newValue);
|
||||||
|
this.agentName = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed("agentAddress1", this.agentAddress1)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed("agentAddress1");
|
||||||
|
actualChanges.put("agentAddress1", newValue);
|
||||||
|
this.agentAddress1 = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed("agentAddress2", this.agentAddress2)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed("agentAddress2");
|
||||||
|
actualChanges.put("agentAddress2", newValue);
|
||||||
|
this.agentAddress2 = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed("agentCity", this.agentCity)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed("agentCity");
|
||||||
|
actualChanges.put("agentCity", newValue);
|
||||||
|
this.agentCity = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed("agentPinCode", this.agentPinCode)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed("agentPinCode");
|
||||||
|
actualChanges.put("agentPinCode", newValue);
|
||||||
|
this.agentPinCode = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed("agentTelephone", this.agentTelephone)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed("agentTelephone");
|
||||||
|
actualChanges.put("agentTelephone", newValue);
|
||||||
|
this.agentTelephone = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed("agentSavingAccountType", this.agentSavingAccountType)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed("agentSavingAccountType");
|
||||||
|
actualChanges.put("agentSavingAccountType", newValue);
|
||||||
|
this.agentSavingAccountType = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInStringParameterNamed("agentSavingAccountNo", this.agentSavingAccountNo)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed("agentSavingAccountNo");
|
||||||
|
actualChanges.put("agentSavingAccountNo", newValue);
|
||||||
|
this.agentSavingAccountNo = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("agentOpeningDate", this.agentOpeningDate)) {
|
||||||
|
actualChanges.put("agentOpeningDate", command.stringValueOfParameterNamed("agentOpeningDate"));
|
||||||
|
this.agentOpeningDate = command.localDateValueOfParameterNamed("agentOpeningDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("agentBirthDate", this.agentBirthDate)) {
|
||||||
|
actualChanges.put("agentBirthDate", command.stringValueOfParameterNamed("agentBirthDate"));
|
||||||
|
this.agentBirthDate = command.localDateValueOfParameterNamed("agentBirthDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "homeBranch", this.homeBranch, v -> this.homeBranch = v);
|
||||||
|
stringChange(command, actualChanges, "savingBranch", this.savingBranch, v -> this.savingBranch = v);
|
||||||
|
|
||||||
|
if (command.isChangeInBooleanParameterNamed("tdsApplicable", this.tdsApplicable)) {
|
||||||
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("tdsApplicable");
|
||||||
|
actualChanges.put("tdsApplicable", newValue);
|
||||||
|
this.tdsApplicable = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("tdsPercent", this.tdsPercent)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("tdsPercent");
|
||||||
|
actualChanges.put("tdsPercent", newValue);
|
||||||
|
this.tdsPercent = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "transferBranch", this.transferBranch,
|
||||||
|
v -> this.transferBranch = StringUtils.defaultIfEmpty(v, null));
|
||||||
|
stringChange(command, actualChanges, "transferAmountAccountType", this.transferAmountAccountType,
|
||||||
|
v -> this.transferAmountAccountType = StringUtils.defaultIfEmpty(v, null));
|
||||||
|
stringChange(command, actualChanges, "transferAccountNo", this.transferAccountNo,
|
||||||
|
v -> this.transferAccountNo = StringUtils.defaultIfEmpty(v, null));
|
||||||
|
|
||||||
|
if (command.isChangeInBooleanParameterNamed("commissionCalculation", this.commissionCalculation)) {
|
||||||
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("commissionCalculation");
|
||||||
|
actualChanges.put("commissionCalculation", newValue);
|
||||||
|
this.commissionCalculation = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("limit", this.limitAmount)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("limit");
|
||||||
|
actualChanges.put("limit", newValue);
|
||||||
|
this.limitAmount = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("machineRent", this.machineRent)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("machineRent");
|
||||||
|
actualChanges.put("machineRent", newValue);
|
||||||
|
this.machineRent = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "agentSecurityBranch", this.agentSecurityBranch,
|
||||||
|
v -> this.agentSecurityBranch = StringUtils.defaultIfEmpty(v, null));
|
||||||
|
stringChange(command, actualChanges, "agentSecurityAccountType", this.agentSecurityAccountType,
|
||||||
|
v -> this.agentSecurityAccountType = StringUtils.defaultIfEmpty(v, null));
|
||||||
|
stringChange(command, actualChanges, "agentSecurityAccountNo", this.agentSecurityAccountNo,
|
||||||
|
v -> this.agentSecurityAccountNo = StringUtils.defaultIfEmpty(v, null));
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("otherCommission", this.otherCommission)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("otherCommission");
|
||||||
|
actualChanges.put("otherCommission", newValue);
|
||||||
|
this.otherCommission = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("agentDeposit", this.agentDeposit)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("agentDeposit");
|
||||||
|
actualChanges.put("agentDeposit", newValue);
|
||||||
|
this.agentDeposit = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("festivalAdvance", this.festivalAdvance)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("festivalAdvance");
|
||||||
|
actualChanges.put("festivalAdvance", newValue);
|
||||||
|
this.festivalAdvance = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("otherDeduction", this.otherDeduction)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("otherDeduction");
|
||||||
|
actualChanges.put("otherDeduction", newValue);
|
||||||
|
this.otherDeduction = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stringChange(final JsonCommand command, final Map<String, Object> actualChanges, final String paramName,
|
||||||
|
final String existingValue, final java.util.function.Consumer<String> setter) {
|
||||||
|
if (command.isChangeInStringParameterNamed(paramName, existingValue)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed(paramName);
|
||||||
|
actualChanges.put(paramName, newValue);
|
||||||
|
setter.accept(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* 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.organisation.agent.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single guarantor recorded against an {@link Agent} (or, reused with the same shape, against a
|
||||||
|
* SubAgent). Always created/replaced as part of its parent agent's own create/update command - it has
|
||||||
|
* no independent API of its own.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_agent_guarantor")
|
||||||
|
public class AgentGuarantor extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "agent_id", nullable = false)
|
||||||
|
private Agent agent;
|
||||||
|
|
||||||
|
@Column(name = "client_id", nullable = false)
|
||||||
|
private Long clientId;
|
||||||
|
|
||||||
|
@Column(name = "guarantor_name", length = 100, nullable = false)
|
||||||
|
private String guarantorName;
|
||||||
|
|
||||||
|
@Column(name = "address1", length = 200, nullable = false)
|
||||||
|
private String address1;
|
||||||
|
|
||||||
|
@Column(name = "address2", length = 200)
|
||||||
|
private String address2;
|
||||||
|
|
||||||
|
@Column(name = "city", length = 100, nullable = false)
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@Column(name = "pin_code", length = 20, nullable = false)
|
||||||
|
private String pinCode;
|
||||||
|
|
||||||
|
@Column(name = "telephone", length = 20)
|
||||||
|
private String telephone;
|
||||||
|
|
||||||
|
protected AgentGuarantor() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgentGuarantor(final Long clientId, final String guarantorName, final String address1, final String address2,
|
||||||
|
final String city, final String pinCode, final String telephone) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.guarantorName = guarantorName;
|
||||||
|
this.address1 = address1;
|
||||||
|
this.address2 = StringUtils.defaultIfEmpty(address2, null);
|
||||||
|
this.city = city;
|
||||||
|
this.pinCode = pinCode;
|
||||||
|
this.telephone = StringUtils.defaultIfEmpty(telephone, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAgent(final Agent agent) {
|
||||||
|
this.agent = agent;
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.organisation.agent.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.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row of an Agent's time-bound collection-limit history (From Date / To Date / Limit) - an
|
||||||
|
* Agent's authorized collection limit can change over time and every change is kept, not overwritten.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_agent_limit")
|
||||||
|
public class AgentLimit extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "agent_id", nullable = false)
|
||||||
|
private Agent agent;
|
||||||
|
|
||||||
|
@Column(name = "from_date", nullable = false)
|
||||||
|
private LocalDate fromDate;
|
||||||
|
|
||||||
|
@Column(name = "to_date", nullable = false)
|
||||||
|
private LocalDate toDate;
|
||||||
|
|
||||||
|
@Column(name = "limit_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal limitAmount;
|
||||||
|
|
||||||
|
protected AgentLimit() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgentLimit(final LocalDate fromDate, final LocalDate toDate, final BigDecimal limitAmount) {
|
||||||
|
this.fromDate = fromDate;
|
||||||
|
this.toDate = toDate;
|
||||||
|
this.limitAmount = limitAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAgent(final Agent agent) {
|
||||||
|
this.agent = agent;
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 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.organisation.agent.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RuntimeException} thrown when a DDS agent resource is not found.
|
||||||
|
*/
|
||||||
|
public class AgentNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public AgentNotFoundException(final Long id) {
|
||||||
|
super("error.msg.agent.id.invalid", "Agent with identifier " + id + " does not exist", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgentNotFoundException(final Long id, final EmptyResultDataAccessException e) {
|
||||||
|
super("error.msg.agent.id.invalid", "Agent with identifier " + id + " does not exist", id, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgentNotFoundException(final String agentCode) {
|
||||||
|
super("error.msg.agent.code.invalid", "Agent with code " + agentCode + " does not exist", agentCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-6
@@ -16,14 +16,24 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.orgsetup.service;
|
package org.apache.fineract.organisation.commissionslab.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
public interface BranchProfileWritePlatformService {
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public final class CommissionSlabData implements Serializable {
|
||||||
|
|
||||||
CommandProcessingResult updateBranchProfile(Long officeId, String branchCode, String branchType, String address,
|
@Serial
|
||||||
String pincode, String contactPhone, String contactEmail, String workingHoursStart, String workingHoursEnd,
|
private static final long serialVersionUID = 1L;
|
||||||
String workingDays, String ifscCode, String micrCode, BigDecimal latitude, BigDecimal longitude);
|
|
||||||
|
private final Long id;
|
||||||
|
private final String slabType;
|
||||||
|
private final BigDecimal slabFrom;
|
||||||
|
private final BigDecimal slabTo;
|
||||||
|
private final BigDecimal commissionPercent;
|
||||||
}
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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.organisation.commissionslab.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row of the agent commission chart: an ACCOUNT slab (commission percentage on collection
|
||||||
|
* volume) or a COMMISSION slab (the override a senior agent earns on a sub-agent's commission).
|
||||||
|
* Append-only reference data, like the legacy screen it replaces - there is deliberately no update or
|
||||||
|
* delete API; a superseded slab range is retired by adding a new row, not editing history.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_commission_slab")
|
||||||
|
public class CommissionSlab extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "slab_type", length = 20, nullable = false)
|
||||||
|
private String slabType;
|
||||||
|
|
||||||
|
@Column(name = "slab_from", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal slabFrom;
|
||||||
|
|
||||||
|
@Column(name = "slab_to", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal slabTo;
|
||||||
|
|
||||||
|
@Column(name = "commission_percent", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal commissionPercent;
|
||||||
|
|
||||||
|
protected CommissionSlab() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommissionSlab(final String slabType, final BigDecimal slabFrom, final BigDecimal slabTo, final BigDecimal commissionPercent) {
|
||||||
|
this.slabType = slabType;
|
||||||
|
this.slabFrom = slabFrom;
|
||||||
|
this.slabTo = slabTo;
|
||||||
|
this.commissionPercent = commissionPercent;
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-12
@@ -27,14 +27,13 @@ import jakarta.ws.rs.PUT;
|
|||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
import org.apache.fineract.command.core.CommandPipeline;
|
||||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
import org.apache.fineract.organisation.monetary.command.CurrencyUpdateCommand;
|
||||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyConfigurationData;
|
import org.apache.fineract.organisation.monetary.data.CurrencyConfigurationData;
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest;
|
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest;
|
||||||
|
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse;
|
||||||
import org.apache.fineract.organisation.monetary.service.OrganisationCurrencyReadPlatformService;
|
import org.apache.fineract.organisation.monetary.service.OrganisationCurrencyReadPlatformService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -45,8 +44,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class CurrenciesApiResource {
|
public class CurrenciesApiResource {
|
||||||
|
|
||||||
private final OrganisationCurrencyReadPlatformService readPlatformService;
|
private final OrganisationCurrencyReadPlatformService readPlatformService;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final CommandPipeline commandPipeline;
|
||||||
private final DefaultToApiJsonSerializer<CurrencyUpdateRequest> toApiJsonSerializer;
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@@ -67,12 +65,13 @@ public class CurrenciesApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Update Currency Configuration", description = "Updates the list of currencies permitted for use.")
|
@Operation(summary = "Update Currency Configuration", description = "Updates the list of currencies permitted for use.")
|
||||||
public String updateCurrencies(@Valid final CurrencyUpdateRequest request) {
|
public CurrencyUpdateResponse updateCurrencies(@Valid CurrencyUpdateRequest request) {
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateCurrency()
|
final var command = new CurrencyUpdateCommand();
|
||||||
.withJson(toApiJsonSerializer.serialize(request)).build();
|
|
||||||
|
|
||||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
command.setPayload(request);
|
||||||
|
|
||||||
return toApiJsonSerializer.serialize(result);
|
final Supplier<CurrencyUpdateResponse> response = commandPipeline.send(command);
|
||||||
|
|
||||||
|
return response.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-63
@@ -1,63 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.organisation.monetary.handler;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.apache.fineract.commands.annotation.CommandType;
|
|
||||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest;
|
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse;
|
|
||||||
import org.apache.fineract.organisation.monetary.service.CurrencyWritePlatformService;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@CommandType(entity = "CURRENCY", action = "UPDATE")
|
|
||||||
public class UpdateCurrencyCommandHandler implements NewCommandSourceHandler {
|
|
||||||
|
|
||||||
private final CurrencyWritePlatformService writePlatformService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public UpdateCurrencyCommandHandler(final CurrencyWritePlatformService writePlatformService) {
|
|
||||||
this.writePlatformService = writePlatformService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@Override
|
|
||||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
|
||||||
|
|
||||||
final String[] currencyCodes = command.arrayValueOfParameterNamed("currencies");
|
|
||||||
final List<String> currencies = currencyCodes == null ? List.of() : Arrays.asList(currencyCodes);
|
|
||||||
|
|
||||||
final CurrencyUpdateRequest request = CurrencyUpdateRequest.builder().currencies(currencies).build();
|
|
||||||
|
|
||||||
final CurrencyUpdateResponse response = this.writePlatformService.updateAllowedCurrencies(request);
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
|
||||||
.withCommandId(command.commandId()) //
|
|
||||||
.with(Map.of("currencies", response.getCurrencies())) //
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -61,15 +61,4 @@ 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-16
@@ -24,10 +24,7 @@ public enum PortfolioProductType {
|
|||||||
SAVING(2, "productType.saving"), //
|
SAVING(2, "productType.saving"), //
|
||||||
CLIENT(5, "productType.client"), //
|
CLIENT(5, "productType.client"), //
|
||||||
PROVISIONING(3, "productType.provisioning"), //
|
PROVISIONING(3, "productType.provisioning"), //
|
||||||
SHARES(4, "productType.shares"), //
|
SHARES(4, "productType.shares"); //
|
||||||
// Current products map their GLs under their own type: sharing SAVING with savings products let a savings
|
|
||||||
// product and a current product with the same id read each other's GL rows.
|
|
||||||
CURRENT(6, "productType.current"); //
|
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -55,25 +52,17 @@ public enum PortfolioProductType {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matched against each constant's own declared value above, not its declaration position --
|
|
||||||
// those two orderings diverge (CLIENT is declared 3rd but its value is 5), so a switch written
|
|
||||||
// by position silently mapped every id past SAVING to the wrong type: 3 (really PROVISIONING)
|
|
||||||
// returned CLIENT, 4 (really SHARES) returned PROVISIONING, 5 (really CLIENT) returned SHARES,
|
|
||||||
// and 6 (CURRENT) fell through to null -- a live NPE the moment a Current-product GL entry
|
|
||||||
// (entity_type_enum=6) reached this on the Journal Entries screen.
|
|
||||||
switch (v) {
|
switch (v) {
|
||||||
case 1:
|
case 1:
|
||||||
return LOAN;
|
return LOAN;
|
||||||
case 2:
|
case 2:
|
||||||
return SAVING;
|
return SAVING;
|
||||||
case 3:
|
case 3:
|
||||||
return PROVISIONING;
|
|
||||||
case 4:
|
|
||||||
return SHARES;
|
|
||||||
case 5:
|
|
||||||
return CLIENT;
|
return CLIENT;
|
||||||
case 6:
|
case 4:
|
||||||
return CURRENT;
|
return PROVISIONING;
|
||||||
|
case 5:
|
||||||
|
return SHARES;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-16
@@ -72,26 +72,11 @@ 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;
|
||||||
|
|||||||
@@ -78,30 +78,6 @@ public class AmlAlert extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "disposition", length = 20)
|
@Column(name = "disposition", length = 20)
|
||||||
private String disposition;
|
private String disposition;
|
||||||
|
|
||||||
/**
|
|
||||||
* The other rules this same transaction matched, comma-separated.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* One transaction raises one alert, under the rule that most warrants attention - otherwise a
|
|
||||||
* single deposit arrives as several alerts and an officer has to dispose of each, with nothing
|
|
||||||
* stopping them clearing one while escalating another. The rules that did not win are kept here
|
|
||||||
* so the case file still shows everything that fired.
|
|
||||||
*/
|
|
||||||
@Column(name = "also_matched_rules", length = 500)
|
|
||||||
private String alsoMatchedRules;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The worst severity band across every rule this transaction matched — 1 Low, 2 Medium, 3 High.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Not the same as the governing rule's own severity. A statutory CTR outranks everything when
|
|
||||||
* the alert is raised, so a high-severity rule can end up recorded in {@link #alsoMatchedRules}
|
|
||||||
* and vanish from the screen. An officer disposing of low and medium alerts in a batch would
|
|
||||||
* then be handed a transaction that had tripped a high-severity rule with nothing to show it.
|
|
||||||
*/
|
|
||||||
@Column(name = "highest_severity")
|
|
||||||
private Integer highestSeverity;
|
|
||||||
|
|
||||||
protected AmlAlert() {}
|
protected AmlAlert() {}
|
||||||
|
|
||||||
public static AmlAlert raise(AmlTransactionType transactionType, Long transactionId, Client client, AmlTransactionRule rule,
|
public static AmlAlert raise(AmlTransactionType transactionType, Long transactionId, Client client, AmlTransactionRule rule,
|
||||||
@@ -118,58 +94,6 @@ public class AmlAlert extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return alert;
|
return alert;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAlsoMatchedRules() {
|
|
||||||
return this.alsoMatchedRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getHighestSeverity() {
|
|
||||||
return this.highestSeverity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordHighestSeverity(final Integer severity) {
|
|
||||||
this.highestSeverity = severity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Truncated to the column width: the record is a convenience, never a reason to fail a save. */
|
|
||||||
public void recordAlsoMatchedRules(final String ruleCodes) {
|
|
||||||
this.alsoMatchedRules = ruleCodes == null || ruleCodes.isBlank() ? null
|
|
||||||
: (ruleCodes.length() > 500 ? ruleCodes.substring(0, 500) : ruleCodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this alert is being worked by somebody other than the given officer.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* An alert under review belongs to whoever opened it until they decide it or hand it back. A second officer
|
|
||||||
* reading the same case and reaching their own verdict is the failure this exists to prevent: the transaction
|
|
||||||
* would carry two reviews, and whichever was written last would be the one on file.
|
|
||||||
*/
|
|
||||||
public boolean isHeldByAnother(final AppUser officer) {
|
|
||||||
return AmlAlertStatus.UNDER_REVIEW.name().equals(this.status) && this.reviewedBy != null && officer != null
|
|
||||||
&& !this.reviewedBy.getId().equals(officer.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Brings the alert into line with the Principal Officer's conclusion on the case it opened.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* An escalation is a referral, not a verdict: it says the AML Officer wanted the case looked at. The verdict is
|
|
||||||
* the Principal Officer's, and when they conclude the case clear the alert has to say so too. Left as ESCALATED,
|
|
||||||
* the feed would go on showing a transaction as suspicious after the bank had decided it was not - and the officer
|
|
||||||
* who cleared it would have no way to see their own conclusion reflected where the alert is worked.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Distinct from {@link #whitelist}: this does not pass the alert's own immutability guard, because it is not a
|
|
||||||
* second officer overturning a decision. It is the case this alert was referred into, coming back with its answer.
|
|
||||||
*/
|
|
||||||
public void applyStrConclusion(final boolean suspicious, final AppUser principalOfficer, final String notes) {
|
|
||||||
this.reviewedBy = principalOfficer;
|
|
||||||
this.reviewedOn = LocalDateTime.now();
|
|
||||||
this.reviewNotes = notes;
|
|
||||||
this.disposition = (suspicious ? AmlAlertDisposition.ESCALATE : AmlAlertDisposition.WHITELIST).name();
|
|
||||||
this.status = (suspicious ? AmlAlertStatus.ESCALATED : AmlAlertStatus.WHITELISTED).name();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void whitelist(AppUser reviewer, String notes) {
|
public void whitelist(AppUser reviewer, String notes) {
|
||||||
this.reviewedBy = reviewer;
|
this.reviewedBy = reviewer;
|
||||||
this.reviewedOn = LocalDateTime.now();
|
this.reviewedOn = LocalDateTime.now();
|
||||||
|
|||||||
-14
@@ -44,18 +44,4 @@ public interface AmlAlertRepository extends JpaRepository<AmlAlert, Long> {
|
|||||||
* twice, which would put two STR cases in front of the Principal Officer for a single transaction.
|
* 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);
|
List<AmlAlert> findByRule_IdAndTransactionId(Long ruleId, Long transactionId);
|
||||||
|
|
||||||
/**
|
|
||||||
* A customer's still-open alerts inside the observation window, oldest first.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* What a recurring-behaviour review acts on. Open is defined as not yet decided rather than by naming the statuses
|
|
||||||
* that are open, so a status this query does not know about is still treated as work: an alert wrongly included
|
|
||||||
* costs a reviewer a glance, one wrongly excluded is a pattern the bank never looked at. A decision about the
|
|
||||||
* customer's pattern must not reopen alerts an officer has already disposed of individually - and oldest first so
|
|
||||||
* that a partial failure leaves the longest-standing ones dealt with.
|
|
||||||
*/
|
|
||||||
@Query("SELECT a FROM AmlAlert a WHERE a.client.id = :clientId AND a.raisedOn >= :since "
|
|
||||||
+ "AND a.status NOT IN ('WHITELISTED', 'ESCALATED') ORDER BY a.raisedOn ASC")
|
|
||||||
List<AmlAlert> findOpenAlertsForClientSince(@Param("clientId") Long clientId, @Param("since") LocalDateTime since);
|
|
||||||
}
|
}
|
||||||
|
|||||||
-163
@@ -1,163 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.useradministration.domain.AppUser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One rule a transaction matched, and what the officer decided about that rule.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The alert is the case; this is a finding within it. A single deposit can trip six rules where five are explained by
|
|
||||||
* the customer's declared business and the sixth is not - one verdict over the whole alert either loses that sixth
|
|
||||||
* observation or sends the other five to an STR they do not belong in.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The rule is held by code rather than by a foreign key to {@link AmlTransactionRule}, and the severity is copied in
|
|
||||||
* rather than read back through it. An alert records what was detected at the time; re-banding or retiring a rule
|
|
||||||
* afterwards must not rewrite what the officer was looking at when they decided.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_aml_alert_rule")
|
|
||||||
public class AmlAlertRule {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "alert_id", nullable = false)
|
|
||||||
private AmlAlert alert;
|
|
||||||
|
|
||||||
@Column(name = "rule_code", nullable = false, length = 100)
|
|
||||||
private String ruleCode;
|
|
||||||
|
|
||||||
@Column(name = "risk_category")
|
|
||||||
private Integer riskCategory;
|
|
||||||
|
|
||||||
@Column(name = "is_governing", nullable = false)
|
|
||||||
private boolean governing;
|
|
||||||
|
|
||||||
@Column(name = "disposition", length = 20)
|
|
||||||
private String disposition;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "reviewed_by")
|
|
||||||
private AppUser reviewedBy;
|
|
||||||
|
|
||||||
@Column(name = "reviewed_on")
|
|
||||||
private LocalDateTime reviewedOn;
|
|
||||||
|
|
||||||
@Column(name = "review_notes", length = 1000)
|
|
||||||
private String reviewNotes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the officer named this rule, as against it being closed to keep the case consistent.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Marking one rule suspicious escalates the transaction and closes every other rule under the same escalation -
|
|
||||||
* nothing may stay open behind a filed STR. Without this the record would read as though all of them had been
|
|
||||||
* examined and condemned, and on a transaction that tripped six rules, which one the officer put their name to is
|
|
||||||
* the substance of the finding.
|
|
||||||
*/
|
|
||||||
@Column(name = "directly_decided", nullable = false)
|
|
||||||
private boolean directlyDecided;
|
|
||||||
|
|
||||||
protected AmlAlertRule() {}
|
|
||||||
|
|
||||||
public static AmlAlertRule of(final AmlAlert alert, final String ruleCode, final Integer riskCategory,
|
|
||||||
final boolean governing) {
|
|
||||||
final AmlAlertRule finding = new AmlAlertRule();
|
|
||||||
finding.alert = alert;
|
|
||||||
finding.ruleCode = ruleCode;
|
|
||||||
finding.riskCategory = riskCategory;
|
|
||||||
finding.governing = governing;
|
|
||||||
return finding;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param directlyDecided
|
|
||||||
* true when the officer named this rule; false when it was closed alongside one they named
|
|
||||||
*/
|
|
||||||
public void decide(final AmlAlertDisposition disposition, final AppUser reviewer, final String notes,
|
|
||||||
final boolean directlyDecided) {
|
|
||||||
this.disposition = disposition.name();
|
|
||||||
this.reviewedBy = reviewer;
|
|
||||||
this.reviewedOn = LocalDateTime.now();
|
|
||||||
this.reviewNotes = notes;
|
|
||||||
this.directlyDecided = directlyDecided;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDirectlyDecided() {
|
|
||||||
return this.directlyDecided;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDecided() {
|
|
||||||
return this.disposition != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEscalated() {
|
|
||||||
return AmlAlertDisposition.ESCALATE.name().equals(this.disposition);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AmlAlert getAlert() {
|
|
||||||
return this.alert;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRuleCode() {
|
|
||||||
return this.ruleCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getRiskCategory() {
|
|
||||||
return this.riskCategory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isGoverning() {
|
|
||||||
return this.governing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisposition() {
|
|
||||||
return this.disposition;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AppUser getReviewedBy() {
|
|
||||||
return this.reviewedBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getReviewedOn() {
|
|
||||||
return this.reviewedOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReviewNotes() {
|
|
||||||
return this.reviewNotes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-106
@@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 far a customer's repetition has gone, and what the bank's answer to it has to be.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A customer who surfaces once is a question; one who surfaces for the fortieth time is an answer the bank has not
|
|
||||||
* acted on. Without a ladder every review looked the same - same screen, same three buttons, same outcome - so the
|
|
||||||
* fortieth review cost an officer the same effort as the first and changed as little.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Each tier does two things: it sets how long a review holds the customer out of the queue, and it says what the
|
|
||||||
* reviewer is expected to do. The windows shorten as the tier rises, because a customer the bank is more worried
|
|
||||||
* about should come back sooner, not later.
|
|
||||||
*/
|
|
||||||
public enum AmlRecurringEscalationTier {
|
|
||||||
|
|
||||||
/** Ordinary repetition. A review stands for a quarter unless the count climbs into the next tier. */
|
|
||||||
ROUTINE(0, 90),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enough repetition that the customer's risk rating is no longer just a KYC-time judgement. Reviewed monthly, and
|
|
||||||
* the reviewer is asked to consider whether the rating still fits.
|
|
||||||
*/
|
|
||||||
ELEVATED(7, 30),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Beyond what a branch officer should be closing on their own. Reviewed fortnightly, and the review is expected
|
|
||||||
* to be taken by someone senior.
|
|
||||||
*/
|
|
||||||
SENIOR(15, 14),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repetition at a level that is itself the suspicion. An STR is the expected outcome, and clearing at this tier
|
|
||||||
* is a decision that has to be defensible on its own - so the review stands for a week only.
|
|
||||||
*/
|
|
||||||
MANDATORY_STR(30, 7);
|
|
||||||
|
|
||||||
private final int minimumAlertCount;
|
|
||||||
private final int reviewValidityDays;
|
|
||||||
|
|
||||||
AmlRecurringEscalationTier(final int minimumAlertCount, final int reviewValidityDays) {
|
|
||||||
this.minimumAlertCount = minimumAlertCount;
|
|
||||||
this.reviewValidityDays = reviewValidityDays;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The tier a customer sitting on this many alerts falls into. */
|
|
||||||
public static AmlRecurringEscalationTier forAlertCount(final int alertCount) {
|
|
||||||
AmlRecurringEscalationTier tier = ROUTINE;
|
|
||||||
for (final AmlRecurringEscalationTier candidate : values()) {
|
|
||||||
if (alertCount >= candidate.minimumAlertCount) {
|
|
||||||
tier = candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMinimumAlertCount() {
|
|
||||||
return minimumAlertCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getReviewValidityDays() {
|
|
||||||
return reviewValidityDays;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How far the alert count may climb before a standing review is set aside and the customer returns early.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A validity window alone would hold a customer out while their behaviour changed underneath it: reviewed at four
|
|
||||||
* alerts and then trips forty more the same week, the review would still stand for the full quarter. Allowing the
|
|
||||||
* count to grow by the tier's own span before forcing a return keeps ordinary repetition quiet while letting a
|
|
||||||
* genuine change in behaviour break through. Tighter at the higher tiers, where less change should be tolerated.
|
|
||||||
*/
|
|
||||||
public int alertGrowthBeforeEarlyReturn() {
|
|
||||||
return switch (this) {
|
|
||||||
case ROUTINE -> 5;
|
|
||||||
case ELEVATED -> 4;
|
|
||||||
case SENIOR -> 3;
|
|
||||||
case MANDATORY_STR -> 2;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whether a review at this tier is expected to raise an STR rather than close the customer off. */
|
|
||||||
public boolean expectsStr() {
|
|
||||||
return this == MANDATORY_STR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-59
@@ -26,7 +26,6 @@ import jakarta.persistence.Id;
|
|||||||
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 java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import org.apache.fineract.portfolio.client.domain.Client;
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
@@ -66,20 +65,6 @@ public class AmlRecurringReview {
|
|||||||
@Column(name = "reviewed_on", nullable = false)
|
@Column(name = "reviewed_on", nullable = false)
|
||||||
private LocalDateTime reviewedOn;
|
private LocalDateTime reviewedOn;
|
||||||
|
|
||||||
/**
|
|
||||||
* The date this review stops standing. Null on reviews taken before validity existed, which keep the older
|
|
||||||
* behaviour of lapsing the moment one more alert lands.
|
|
||||||
*/
|
|
||||||
@Column(name = "valid_until")
|
|
||||||
private LocalDate validUntil;
|
|
||||||
|
|
||||||
@Column(name = "escalation_tier", length = 20)
|
|
||||||
private String escalationTier;
|
|
||||||
|
|
||||||
/** How many of the customer's open alerts this review actually disposed of. */
|
|
||||||
@Column(name = "alerts_actioned")
|
|
||||||
private Integer alertsActioned;
|
|
||||||
|
|
||||||
protected AmlRecurringReview() {}
|
protected AmlRecurringReview() {}
|
||||||
|
|
||||||
public static AmlRecurringReview record(final Client client, final int alertCountAtReview,
|
public static AmlRecurringReview record(final Client client, final int alertCountAtReview,
|
||||||
@@ -91,22 +76,9 @@ public class AmlRecurringReview {
|
|||||||
review.notes = notes;
|
review.notes = notes;
|
||||||
review.reviewedBy = reviewedBy;
|
review.reviewedBy = reviewedBy;
|
||||||
review.reviewedOn = LocalDateTime.now();
|
review.reviewedOn = LocalDateTime.now();
|
||||||
|
|
||||||
// The tier follows from the count, not from what the reviewer picked: how often a customer has surfaced is a
|
|
||||||
// fact about them, and letting the outcome set it would let a customer be held at ROUTINE indefinitely by
|
|
||||||
// always being cleared.
|
|
||||||
final AmlRecurringEscalationTier tier = AmlRecurringEscalationTier.forAlertCount(alertCountAtReview);
|
|
||||||
review.escalationTier = tier.name();
|
|
||||||
review.validUntil = LocalDate.now().plusDays(tier.getReviewValidityDays());
|
|
||||||
review.alertsActioned = 0;
|
|
||||||
return review;
|
return review;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Records how many open alerts the review disposed of, once the caller has acted on them. */
|
|
||||||
public void recordAlertsActioned(final int count) {
|
|
||||||
this.alertsActioned = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -134,35 +106,4 @@ public class AmlRecurringReview {
|
|||||||
public LocalDateTime getReviewedOn() {
|
public LocalDateTime getReviewedOn() {
|
||||||
return reviewedOn;
|
return reviewedOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalDate getValidUntil() {
|
|
||||||
return validUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AmlRecurringEscalationTier getEscalationTier() {
|
|
||||||
return escalationTier == null ? AmlRecurringEscalationTier.ROUTINE : AmlRecurringEscalationTier.valueOf(escalationTier);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getAlertsActioned() {
|
|
||||||
return alertsActioned;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this review still holds the customer out of the review queue.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Two ways it stops standing. Its window runs out, or the customer's behaviour moves on far enough that the
|
|
||||||
* review no longer describes them - judged by how much the alert count has grown since, against the allowance for
|
|
||||||
* the tier it was taken at. A review with no window at all is a pre-existing one, and keeps the original rule of
|
|
||||||
* lapsing as soon as one more alert arrives.
|
|
||||||
*/
|
|
||||||
public boolean stillStands(final int currentAlertCount) {
|
|
||||||
if (validUntil == null) {
|
|
||||||
return currentAlertCount <= alertCountAtReview;
|
|
||||||
}
|
|
||||||
if (LocalDate.now().isAfter(validUntil)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return currentAlertCount - alertCountAtReview < getEscalationTier().alertGrowthBeforeEarlyReturn();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-83
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One alert covered by an STR case.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A case escalated from a single transaction covers that one alert. A case raised off a customer's repeated behaviour
|
|
||||||
* covers all the alerts that make up the pattern - which is what an STR to FIU-IND actually describes, and what the
|
|
||||||
* Principal Officer has to be able to read in one place.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_aml_str_case_alert")
|
|
||||||
public class AmlStrCaseAlert {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
|
||||||
@JoinColumn(name = "str_case_id", nullable = false)
|
|
||||||
private AmlStrCase strCase;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
|
||||||
@JoinColumn(name = "alert_id", nullable = false)
|
|
||||||
private AmlAlert alert;
|
|
||||||
|
|
||||||
@Column(name = "added_on_utc", nullable = false)
|
|
||||||
private LocalDateTime addedOn;
|
|
||||||
|
|
||||||
protected AmlStrCaseAlert() {}
|
|
||||||
|
|
||||||
public static AmlStrCaseAlert link(final AmlStrCase strCase, final AmlAlert alert) {
|
|
||||||
final AmlStrCaseAlert link = new AmlStrCaseAlert();
|
|
||||||
link.strCase = strCase;
|
|
||||||
link.alert = alert;
|
|
||||||
link.addedOn = LocalDateTime.now();
|
|
||||||
return link;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AmlStrCase getStrCase() {
|
|
||||||
return strCase;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AmlAlert getAlert() {
|
|
||||||
return alert;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getAddedOn() {
|
|
||||||
return addedOn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-4
@@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.aml.domain;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
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;
|
||||||
@@ -29,9 +28,6 @@ public interface AmlStrCaseRepository extends JpaRepository<AmlStrCase, Long> {
|
|||||||
|
|
||||||
long countByClient_Id(Long clientId);
|
long countByClient_Id(Long clientId);
|
||||||
|
|
||||||
/** The case already open for a transaction, so escalating it again does not open a second one. */
|
|
||||||
Optional<AmlStrCase> findByAlertId(Long alertId);
|
|
||||||
|
|
||||||
List<AmlStrCase> findByStatus(String status);
|
List<AmlStrCase> findByStatus(String status);
|
||||||
|
|
||||||
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.concludedOn BETWEEN :from AND :to")
|
@Query("SELECT c FROM AmlStrCase c WHERE c.status = 'CONCLUDED_SUSPICIOUS' AND c.concludedOn BETWEEN :from AND :to")
|
||||||
|
|||||||
-43
@@ -84,49 +84,6 @@ public class AmlTransactionRule {
|
|||||||
@Column(name = "is_active", nullable = false)
|
@Column(name = "is_active", nullable = false)
|
||||||
private boolean active;
|
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() {}
|
protected AmlTransactionRule() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
-7
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.aml.domain;
|
package org.apache.fineract.portfolio.aml.domain;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
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,10 +32,4 @@ public interface AmlTransactionRuleRepository extends JpaRepository<AmlTransacti
|
|||||||
* by hand from the ledger.
|
* by hand from the ledger.
|
||||||
*/
|
*/
|
||||||
Optional<AmlTransactionRule> findByRuleCode(String ruleCode);
|
Optional<AmlTransactionRule> findByRuleCode(String ruleCode);
|
||||||
|
|
||||||
/**
|
|
||||||
* Every rule named in the given codes, in one read. The alert screen resolves the rules each transaction matched
|
|
||||||
* into descriptions and severities; doing that a code at a time would be one query per rule per alert.
|
|
||||||
*/
|
|
||||||
List<AmlTransactionRule> findByRuleCodeIn(Collection<String> ruleCodes);
|
|
||||||
}
|
}
|
||||||
|
|||||||
-9
@@ -162,15 +162,6 @@ 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";
|
||||||
|
|||||||
-219
@@ -1,219 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.Table;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What one customer's money actually did over a period, and what that says about their rating.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A risk rating describes behaviour, and behaviour is a period rather than an event. Judging it from the alert feed
|
|
||||||
* meant a customer appeared the moment one transaction tripped a rule and was recommended for re-rating on that alone
|
|
||||||
* - a single shop purchase could argue for lowering a High rating, and a single large deposit for raising a Low one.
|
|
||||||
* Neither is a pattern.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* One row is one window for one customer, computed nightly. Upgrades and downgrades read different windows, because
|
|
||||||
* the questions are not symmetric: a concern is about what the customer is doing now, and confidence needs a year of
|
|
||||||
* it. The suggestion is stored with the figures it was drawn from, so an officer reviewing it months later sees what
|
|
||||||
* the system was looking at rather than a conclusion with no working.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_client_behaviour_window")
|
|
||||||
public class ClientBehaviourWindow {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "client_id", nullable = false)
|
|
||||||
private Long clientId;
|
|
||||||
|
|
||||||
@Column(name = "window_from", nullable = false)
|
|
||||||
private LocalDate windowFrom;
|
|
||||||
|
|
||||||
@Column(name = "window_to", nullable = false)
|
|
||||||
private LocalDate windowTo;
|
|
||||||
|
|
||||||
@Column(name = "txn_count")
|
|
||||||
private Integer txnCount;
|
|
||||||
|
|
||||||
@Column(name = "distinct_months")
|
|
||||||
private Integer distinctMonths;
|
|
||||||
|
|
||||||
@Column(name = "credit_total", precision = 19, scale = 6)
|
|
||||||
private BigDecimal creditTotal;
|
|
||||||
|
|
||||||
@Column(name = "debit_total", precision = 19, scale = 6)
|
|
||||||
private BigDecimal debitTotal;
|
|
||||||
|
|
||||||
@Column(name = "largest_txn", precision = 19, scale = 6)
|
|
||||||
private BigDecimal largestTxn;
|
|
||||||
|
|
||||||
@Column(name = "cash_txn_count")
|
|
||||||
private Integer cashTxnCount;
|
|
||||||
|
|
||||||
@Column(name = "cash_total", precision = 19, scale = 6)
|
|
||||||
private BigDecimal cashTotal;
|
|
||||||
|
|
||||||
@Column(name = "large_txn_count")
|
|
||||||
private Integer largeTxnCount;
|
|
||||||
|
|
||||||
@Column(name = "declared_annual_income", precision = 19, scale = 6)
|
|
||||||
private BigDecimal declaredAnnualIncome;
|
|
||||||
|
|
||||||
@Column(name = "declared_annual_turnover", precision = 19, scale = 6)
|
|
||||||
private BigDecimal declaredAnnualTurnover;
|
|
||||||
|
|
||||||
@Column(name = "income_ratio", precision = 19, scale = 6)
|
|
||||||
private BigDecimal incomeRatio;
|
|
||||||
|
|
||||||
@Column(name = "alert_count")
|
|
||||||
private Integer alertCount;
|
|
||||||
|
|
||||||
@Column(name = "str_case_count")
|
|
||||||
private Integer strCaseCount;
|
|
||||||
|
|
||||||
@Column(name = "risk_category_at_review")
|
|
||||||
private Integer riskCategoryAtReview;
|
|
||||||
|
|
||||||
/** UP, DOWN or HOLD. */
|
|
||||||
@Column(name = "suggested_direction", length = 10)
|
|
||||||
private String suggestedDirection;
|
|
||||||
|
|
||||||
@Column(name = "suggested_category")
|
|
||||||
private Integer suggestedCategory;
|
|
||||||
|
|
||||||
@Column(name = "suggestion_reason", length = 500)
|
|
||||||
private String suggestionReason;
|
|
||||||
|
|
||||||
@Column(name = "computed_on")
|
|
||||||
private LocalDateTime computedOn;
|
|
||||||
|
|
||||||
protected ClientBehaviourWindow() {}
|
|
||||||
|
|
||||||
public static ClientBehaviourWindow of(final Long clientId, final LocalDate windowFrom, final LocalDate windowTo) {
|
|
||||||
final ClientBehaviourWindow window = new ClientBehaviourWindow();
|
|
||||||
window.clientId = clientId;
|
|
||||||
window.windowFrom = windowFrom;
|
|
||||||
window.windowTo = windowTo;
|
|
||||||
window.computedOn = LocalDateTime.now();
|
|
||||||
return window;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordDistinctMonths(final Integer months) {
|
|
||||||
this.distinctMonths = months;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getDistinctMonths() {
|
|
||||||
return this.distinctMonths;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordActivity(final Integer txnCount, final BigDecimal creditTotal, final BigDecimal debitTotal,
|
|
||||||
final BigDecimal largestTxn, final Integer cashTxnCount, final BigDecimal cashTotal, final Integer largeTxnCount) {
|
|
||||||
this.txnCount = txnCount;
|
|
||||||
this.creditTotal = creditTotal;
|
|
||||||
this.debitTotal = debitTotal;
|
|
||||||
this.largestTxn = largestTxn;
|
|
||||||
this.cashTxnCount = cashTxnCount;
|
|
||||||
this.cashTotal = cashTotal;
|
|
||||||
this.largeTxnCount = largeTxnCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordMonitoring(final Integer alertCount, final Integer strCaseCount, final Integer riskCategoryAtReview) {
|
|
||||||
this.alertCount = alertCount;
|
|
||||||
this.strCaseCount = strCaseCount;
|
|
||||||
this.riskCategoryAtReview = riskCategoryAtReview;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turnover measured against what the customer said to expect.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Null rather than zero when no income was declared: the ratio is unknown, not nil, and a rule that treats an
|
|
||||||
* unknown as a benign zero would clear every customer the bank never asked.
|
|
||||||
*/
|
|
||||||
public void recordDeclared(final BigDecimal income, final BigDecimal turnover, final BigDecimal periodTurnover) {
|
|
||||||
this.declaredAnnualIncome = income;
|
|
||||||
this.declaredAnnualTurnover = turnover;
|
|
||||||
final BigDecimal basis = income != null && income.signum() > 0 ? income
|
|
||||||
: (turnover != null && turnover.signum() > 0 ? turnover : null);
|
|
||||||
this.incomeRatio = basis == null || periodTurnover == null ? null
|
|
||||||
: periodTurnover.divide(basis, 6, java.math.RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordSuggestion(final String direction, final Integer category, final String reason) {
|
|
||||||
this.suggestedDirection = direction;
|
|
||||||
this.suggestedCategory = category;
|
|
||||||
this.suggestionReason = reason == null || reason.length() <= 500 ? reason : reason.substring(0, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getClientId() {
|
|
||||||
return this.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDate getWindowFrom() {
|
|
||||||
return this.windowFrom;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDate getWindowTo() {
|
|
||||||
return this.windowTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getTxnCount() {
|
|
||||||
return this.txnCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getCashTotal() {
|
|
||||||
return this.cashTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getLargeTxnCount() {
|
|
||||||
return this.largeTxnCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getAlertCount() {
|
|
||||||
return this.alertCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getStrCaseCount() {
|
|
||||||
return this.strCaseCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getRiskCategoryAtReview() {
|
|
||||||
return this.riskCategoryAtReview;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSuggestedDirection() {
|
|
||||||
return this.suggestedDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getSuggestedCategory() {
|
|
||||||
return this.suggestedCategory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
public interface ClientBehaviourWindowRepository extends JpaRepository<ClientBehaviourWindow, Long> {
|
|
||||||
|
|
||||||
List<ClientBehaviourWindow> findByClientId(Long clientId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the windows computed for a given day before the job writes that day's.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The job is a recomputation, not an append. Re-running it - after a failure, or twice in a night - must leave one
|
|
||||||
* set of windows rather than a customer appearing several times on the review screen with slightly different
|
|
||||||
* figures.
|
|
||||||
*/
|
|
||||||
@Modifying
|
|
||||||
@Query("DELETE FROM ClientBehaviourWindow w WHERE w.windowTo = :windowTo")
|
|
||||||
void deleteByWindowTo(@Param("windowTo") LocalDate windowTo);
|
|
||||||
}
|
|
||||||
+15
-171
@@ -291,31 +291,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@Column(name = "form15_fy", length = 10)
|
@Column(name = "form15_fy", length = 10)
|
||||||
private String form15Fy;
|
private String form15Fy;
|
||||||
|
|
||||||
// ── Risk profile ─────────────────────────────────────────────────────────
|
|
||||||
/**
|
|
||||||
* Customer facts the risk re-rating policy tests against, as against anything the customer transacted.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A politically exposed person is not downgraded because their activity looks ordinary — that is precisely the
|
|
||||||
* risk — and the same holds for a sanctions or adverse-media match. False means "not recorded as such", which is
|
|
||||||
* the correct reading for a book where nobody was ever asked; it is not an assertion that the customer is clean.
|
|
||||||
*/
|
|
||||||
@Column(name = "is_pep", nullable = false)
|
|
||||||
private boolean isPep;
|
|
||||||
|
|
||||||
@Column(name = "sanctions_match", nullable = false)
|
|
||||||
private boolean sanctionsMatch;
|
|
||||||
|
|
||||||
@Column(name = "adverse_media_match", nullable = false)
|
|
||||||
private boolean adverseMediaMatch;
|
|
||||||
|
|
||||||
/** What the customer told the bank to expect, which their actual turnover is measured against. */
|
|
||||||
@Column(name = "declared_annual_income", precision = 19, scale = 6)
|
|
||||||
private BigDecimal declaredAnnualIncome;
|
|
||||||
|
|
||||||
@Column(name = "declared_annual_turnover", precision = 19, scale = 6)
|
|
||||||
private BigDecimal declaredAnnualTurnover;
|
|
||||||
|
|
||||||
// ── CKYCRR ───────────────────────────────────────────────────────────────
|
// ── CKYCRR ───────────────────────────────────────────────────────────────
|
||||||
/** Central KYC Records Registry number issued by CERSAI */
|
/** Central KYC Records Registry number issued by CERSAI */
|
||||||
@Column(name = "ckyc_number", length = 14)
|
@Column(name = "ckyc_number", length = 14)
|
||||||
@@ -380,7 +355,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
this.dinVerified = false;
|
this.dinVerified = false;
|
||||||
this.isException = false;
|
this.isException = false;
|
||||||
this.exceptionResolved = false;
|
this.exceptionResolved = false;
|
||||||
this.initiatedOn = DateUtils.getBusinessLocalDate();
|
this.initiatedOn = DateUtils.getLocalDateOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
|
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
|
||||||
@@ -509,14 +484,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
actualChanges.put(riskParam, newValue);
|
actualChanges.put(riskParam, newValue);
|
||||||
recordOverwrite(riskParam, this.riskCategory);
|
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
|
// remarks is the audit justification itself, so a change to it is deliberately not recorded
|
||||||
@@ -552,19 +519,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
v -> this.aadhaarReceiptRemarks = v);
|
v -> this.aadhaarReceiptRemarks = v);
|
||||||
updateDateField(command, "aadhaarReceiptTakenOn", this.aadhaarReceiptTakenOn, actualChanges,
|
updateDateField(command, "aadhaarReceiptTakenOn", this.aadhaarReceiptTakenOn, actualChanges,
|
||||||
v -> this.aadhaarReceiptTakenOn = v);
|
v -> this.aadhaarReceiptTakenOn = v);
|
||||||
// Risk profile. These are determinations about the customer rather than about a transaction, and the
|
|
||||||
// re-rating policy reads them directly: a PEP or a sanctions match holds a rating at High however ordinary the
|
|
||||||
// period's activity looks.
|
|
||||||
updateBooleanField(command, "isPep", this.isPep, actualChanges, v -> this.isPep = v != null ? v : false);
|
|
||||||
updateBooleanField(command, "sanctionsMatch", this.sanctionsMatch, actualChanges,
|
|
||||||
v -> this.sanctionsMatch = v != null ? v : false);
|
|
||||||
updateBooleanField(command, "adverseMediaMatch", this.adverseMediaMatch, actualChanges,
|
|
||||||
v -> this.adverseMediaMatch = v != null ? v : false);
|
|
||||||
updateBigDecimalField(command, "declaredAnnualIncome", this.declaredAnnualIncome, actualChanges,
|
|
||||||
v -> this.declaredAnnualIncome = v);
|
|
||||||
updateBigDecimalField(command, "declaredAnnualTurnover", this.declaredAnnualTurnover, actualChanges,
|
|
||||||
v -> this.declaredAnnualTurnover = 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);
|
||||||
@@ -600,21 +554,10 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
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
|
updateBooleanField(command, "isException", this.isException, actualChanges, v -> this.isException = v != null ? v : false);
|
||||||
// data an officer types, so they are updated without recording an overwrite. A verified
|
updateStringField(command, "exceptionReason", this.exceptionReason, actualChanges, v -> this.exceptionReason = v);
|
||||||
// record demands remarks for any field it already held (see
|
updateBooleanField(command, "exceptionResolved", this.exceptionResolved, actualChanges, v -> this.exceptionResolved = v != null ? v : false);
|
||||||
// getFieldsOverwrittenByLastUpdate), and is_exception is never null once written — so
|
updateDateField(command, "exceptionResolvedOn", this.exceptionResolvedOn, actualChanges, v -> this.exceptionResolvedOn = v);
|
||||||
// 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, "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, "aadhaarVerified", this.aadhaarVerified, actualChanges, v -> this.aadhaarVerified = v != null ? v : false);
|
||||||
updateBooleanField(command, "gstinVerified", this.gstinVerified, actualChanges, v -> this.gstinVerified = v != null ? v : false);
|
updateBooleanField(command, "gstinVerified", this.gstinVerified, actualChanges, v -> this.gstinVerified = v != null ? v : false);
|
||||||
@@ -669,44 +612,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
* populated is the record being completed rather than altered, so it is deliberately not
|
* populated is the record being completed rather than altered, so it is deliberately not
|
||||||
* recorded — see {@link #getFieldsOverwrittenByLastUpdate()}.
|
* 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) {
|
private void recordOverwrite(final String param, final Object currentValue) {
|
||||||
final boolean wasPopulated = currentValue instanceof String text
|
final boolean wasPopulated = currentValue instanceof String text
|
||||||
? StringUtils.isNotBlank(text)
|
? StringUtils.isNotBlank(text)
|
||||||
@@ -757,13 +662,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 = DateUtils.getBusinessLocalDate();
|
this.ckycUploadedOn = DateUtils.getLocalDateOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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 = DateUtils.getBusinessLocalDate();
|
this.verifiedOn = DateUtils.getLocalDateOfTenant();
|
||||||
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);
|
||||||
@@ -772,45 +677,17 @@ 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 = DateUtils.getBusinessLocalDate();
|
this.rejectedOn = DateUtils.getLocalDateOfTenant();
|
||||||
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 = DateUtils.getBusinessLocalDate();
|
this.panVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||||
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);
|
||||||
@@ -836,28 +713,28 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
|
|
||||||
public void markAadhaarVerified() {
|
public void markAadhaarVerified() {
|
||||||
this.aadhaarVerified = true;
|
this.aadhaarVerified = true;
|
||||||
this.aadhaarVerifiedOn = DateUtils.getBusinessLocalDate();
|
this.aadhaarVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markGstinVerified() {
|
public void markGstinVerified() {
|
||||||
this.gstinVerified = true;
|
this.gstinVerified = true;
|
||||||
this.gstinVerifiedOn = DateUtils.getBusinessLocalDate();
|
this.gstinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markCinVerified() {
|
public void markCinVerified() {
|
||||||
this.cinVerified = true;
|
this.cinVerified = true;
|
||||||
this.cinVerifiedOn = DateUtils.getBusinessLocalDate();
|
this.cinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markDinVerified() {
|
public void markDinVerified() {
|
||||||
this.dinVerified = true;
|
this.dinVerified = true;
|
||||||
this.dinVerifiedOn = DateUtils.getBusinessLocalDate();
|
this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = DateUtils.getBusinessLocalDate();
|
this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||||
this.isDirector = true;
|
this.isDirector = true;
|
||||||
this.dinMcaResponse = mcaResponseJson;
|
this.dinMcaResponse = mcaResponseJson;
|
||||||
}
|
}
|
||||||
@@ -878,17 +755,7 @@ 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;
|
||||||
}
|
}
|
||||||
// Re-stamped even when the record already reads VERIFIED, because that is exactly what a
|
if (allDone && !getKycStatus().isVerified()) {
|
||||||
// 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;
|
||||||
@@ -931,29 +798,6 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
*/
|
*/
|
||||||
public void escalateRiskCategory(Integer newCategory) {
|
public void escalateRiskCategory(Integer newCategory) {
|
||||||
this.riskCategory = 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 boolean isPep() {
|
|
||||||
return this.isPep;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSanctionsMatch() {
|
|
||||||
return this.sanctionsMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAdverseMediaMatch() {
|
|
||||||
return this.adverseMediaMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getDeclaredAnnualIncome() {
|
|
||||||
return this.declaredAnnualIncome;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getDeclaredAnnualTurnover() {
|
|
||||||
return this.declaredAnnualTurnover;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPanNumber() {
|
public String getPanNumber() {
|
||||||
|
|||||||
-13
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
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;
|
||||||
@@ -49,18 +48,6 @@ public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
|
|||||||
@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
|
||||||
|
|||||||
+1
-35
@@ -93,26 +93,6 @@ public class ClientRiskRule {
|
|||||||
@Column(name = "order_position", nullable = false)
|
@Column(name = "order_position", nullable = false)
|
||||||
private Integer orderPosition;
|
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() {}
|
protected ClientRiskRule() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,9 +104,8 @@ public class ClientRiskRule {
|
|||||||
final BigDecimal maxMonthlyIncome, final BigDecimal minAnnualIncome, final BigDecimal maxAnnualIncome,
|
final BigDecimal maxMonthlyIncome, final BigDecimal minAnnualIncome, final BigDecimal maxAnnualIncome,
|
||||||
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover, final String amlCheckStatus,
|
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover, final String amlCheckStatus,
|
||||||
final String bannedEntityMatch, final String willfulDefaulter, final String ecgcSalFlag,
|
final String bannedEntityMatch, final String willfulDefaulter, final String ecgcSalFlag,
|
||||||
final String dormantFlag, final String ruleKind) {
|
final String dormantFlag) {
|
||||||
final ClientRiskRule rule = new ClientRiskRule();
|
final ClientRiskRule rule = new ClientRiskRule();
|
||||||
rule.ruleKind = ruleKind;
|
|
||||||
rule.ruleName = ruleName;
|
rule.ruleName = ruleName;
|
||||||
rule.reason = reason;
|
rule.reason = reason;
|
||||||
rule.riskCategory = riskCategory;
|
rule.riskCategory = riskCategory;
|
||||||
@@ -158,19 +137,6 @@ public class ClientRiskRule {
|
|||||||
return id;
|
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() {
|
public String getRuleName() {
|
||||||
return ruleName;
|
return ruleName;
|
||||||
}
|
}
|
||||||
|
|||||||
-106
@@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-74
@@ -1,74 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.Table;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One number the risk re-rating policy is made of - a window length, a minimum transaction count, a cash threshold.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Held as data because these are the bank's risk appetite rather than engineering constants. How long a customer must
|
|
||||||
* behave before a High rating comes down is a compliance judgement, and a co-operative bank whose members transact
|
|
||||||
* three times a month needs different figures from one lending to traders.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_risk_policy_parameter")
|
|
||||||
public class RiskPolicyParameter {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "parameter_key", nullable = false, length = 80)
|
|
||||||
private String parameterKey;
|
|
||||||
|
|
||||||
@Column(name = "parameter_value", nullable = false, precision = 19, scale = 2)
|
|
||||||
private BigDecimal parameterValue;
|
|
||||||
|
|
||||||
@Column(name = "description", length = 500)
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
@Column(name = "last_modified_by")
|
|
||||||
private Long lastModifiedBy;
|
|
||||||
|
|
||||||
@Column(name = "last_modified_on")
|
|
||||||
private LocalDateTime lastModifiedOn;
|
|
||||||
|
|
||||||
protected RiskPolicyParameter() {}
|
|
||||||
|
|
||||||
public String getParameterKey() {
|
|
||||||
return this.parameterKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getParameterValue() {
|
|
||||||
return this.parameterValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return this.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.batch.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Collection;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
public final class BatchRunData implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String jobType;
|
||||||
|
private final String branchScope;
|
||||||
|
private final LocalDate jobDate;
|
||||||
|
private final Long agentFrom;
|
||||||
|
private final Long agentTo;
|
||||||
|
private final Long partyFrom;
|
||||||
|
private final Long partyTo;
|
||||||
|
private final String status;
|
||||||
|
private final Integer totalRows;
|
||||||
|
private final BigDecimal totalAmount;
|
||||||
|
private final LocalDate generatedOnDate;
|
||||||
|
private final LocalDate postedOnDate;
|
||||||
|
private final Collection<BatchRunRowData> rows;
|
||||||
|
}
|
||||||
+18
-7
@@ -16,15 +16,26 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.useradministration.domain;
|
package org.apache.fineract.portfolio.dds.batch.data;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.io.Serial;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import java.io.Serializable;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
public interface UserPendingRequestRepository extends JpaRepository<UserPendingRequest, Long>, JpaSpecificationExecutor<UserPendingRequest> {
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public final class BatchRunRowData implements Serializable {
|
||||||
|
|
||||||
Optional<UserPendingRequest> findByPendingLockKey(String pendingLockKey);
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
boolean existsByPendingLockKey(String pendingLockKey);
|
private final Long id;
|
||||||
|
private final Long partyId;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String partyName;
|
||||||
|
private final Long agentId;
|
||||||
|
private final String agentCode;
|
||||||
|
private final BigDecimal amount;
|
||||||
}
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.batch.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.CascadeType;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OrderBy;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One Calculate-then-Post batch run - the deliberate two-step control from the legacy Interest Part
|
||||||
|
* menu (Interest Calculation -> Interest Posting, OD Interest Calculation -> OD Interest Posting,
|
||||||
|
* Commission Calculation -> Commission Posting): a run is generated (rows computed and saved as a
|
||||||
|
* preview, nothing else changes), then posted (a separate, explicit step that applies exactly the
|
||||||
|
* rows that were previewed - never a fresh recalculation, so what gets applied is always what was
|
||||||
|
* seen).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_batch_run")
|
||||||
|
public class BatchRun extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "job_type", length = 40, nullable = false)
|
||||||
|
private String jobType;
|
||||||
|
|
||||||
|
@Column(name = "branch_scope", length = 20, nullable = false)
|
||||||
|
private String branchScope;
|
||||||
|
|
||||||
|
@Column(name = "job_date", nullable = false)
|
||||||
|
private LocalDate jobDate;
|
||||||
|
|
||||||
|
@Column(name = "agent_from", nullable = false)
|
||||||
|
private Long agentFrom;
|
||||||
|
|
||||||
|
@Column(name = "agent_to", nullable = false)
|
||||||
|
private Long agentTo;
|
||||||
|
|
||||||
|
@Column(name = "party_from")
|
||||||
|
private Long partyFrom;
|
||||||
|
|
||||||
|
@Column(name = "party_to")
|
||||||
|
private Long partyTo;
|
||||||
|
|
||||||
|
@Column(name = "status", length = 20, nullable = false)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "total_rows", nullable = false)
|
||||||
|
private Integer totalRows;
|
||||||
|
|
||||||
|
@Column(name = "total_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
|
@Column(name = "generated_on_date", nullable = false)
|
||||||
|
private LocalDate generatedOnDate;
|
||||||
|
|
||||||
|
@Column(name = "posted_on_date")
|
||||||
|
private LocalDate postedOnDate;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "batchRun", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
|
@OrderBy("id")
|
||||||
|
private List<BatchRunRow> rows = new ArrayList<>();
|
||||||
|
|
||||||
|
protected BatchRun() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public BatchRun(final String jobType, final String branchScope, final LocalDate jobDate, final Long agentFrom,
|
||||||
|
final Long agentTo, final Long partyFrom, final Long partyTo, final LocalDate generatedOnDate) {
|
||||||
|
this.jobType = jobType;
|
||||||
|
this.branchScope = branchScope;
|
||||||
|
this.jobDate = jobDate;
|
||||||
|
this.agentFrom = agentFrom;
|
||||||
|
this.agentTo = agentTo;
|
||||||
|
this.partyFrom = partyFrom;
|
||||||
|
this.partyTo = partyTo;
|
||||||
|
this.status = "CALCULATED";
|
||||||
|
this.totalRows = 0;
|
||||||
|
this.totalAmount = BigDecimal.ZERO;
|
||||||
|
this.generatedOnDate = generatedOnDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRow(final BatchRunRow row) {
|
||||||
|
row.setBatchRun(this);
|
||||||
|
this.rows.add(row);
|
||||||
|
this.totalRows = this.rows.size();
|
||||||
|
this.totalAmount = this.totalAmount.add(row.getAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPosted() {
|
||||||
|
return "POSTED".equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markPosted(final LocalDate postedOnDate) {
|
||||||
|
this.status = "POSTED";
|
||||||
|
this.postedOnDate = postedOnDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.batch.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 lombok.Getter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One previewed/posted line of a {@link BatchRun}: for an interest job, one Party's accrued interest;
|
||||||
|
* for a commission job, one Agent's commission on their collected volume (partyId is null there).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_batch_run_row")
|
||||||
|
public class BatchRunRow extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "batch_run_id", nullable = false)
|
||||||
|
private BatchRun batchRun;
|
||||||
|
|
||||||
|
@Column(name = "party_id")
|
||||||
|
private Long partyId;
|
||||||
|
|
||||||
|
@Column(name = "agent_id", nullable = false)
|
||||||
|
private Long agentId;
|
||||||
|
|
||||||
|
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
protected BatchRunRow() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public BatchRunRow(final Long partyId, final Long agentId, final BigDecimal amount) {
|
||||||
|
this.partyId = partyId;
|
||||||
|
this.agentId = agentId;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setBatchRun(final BatchRun batchRun) {
|
||||||
|
this.batchRun = batchRun;
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.batch.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when Post is attempted on a batch run that has already been posted - a run applies once,
|
||||||
|
* matching the calculate-then-post safety control (never re-post the same preview).
|
||||||
|
*/
|
||||||
|
public class BatchRunAlreadyPostedException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public BatchRunAlreadyPostedException(final Long batchRunId) {
|
||||||
|
super("error.msg.ddsbatchrun.already.posted", "DDS batch run " + batchRunId + " has already been posted", batchRunId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -16,13 +16,13 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.useradministration.exception;
|
package org.apache.fineract.portfolio.dds.batch.exception;
|
||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
|
||||||
public class UserPendingRequestNotFoundException extends AbstractPlatformResourceNotFoundException {
|
public class BatchRunNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
public UserPendingRequestNotFoundException(final Long id) {
|
public BatchRunNotFoundException(final Long id) {
|
||||||
super("error.msg.userpendingrequest.id.invalid", "User pending request with identifier " + id + " does not exist", id);
|
super("error.msg.ddsbatchrun.id.invalid", "DDS batch run with identifier " + id + " does not exist", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.party.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Collection;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable data object representing a DDS Party (customer scheme account), including its nominees.
|
||||||
|
* Built via the Lombok builder rather than a long positional constructor - the field count here makes
|
||||||
|
* positional args unreadable and error-prone to keep in sync.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder(toBuilder = true)
|
||||||
|
public final class PartyData implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long clientId;
|
||||||
|
private final String clientName;
|
||||||
|
private final Long agentId;
|
||||||
|
private final String agentCode;
|
||||||
|
private final String agentName;
|
||||||
|
private final Long savingsAccountId;
|
||||||
|
private final String subType;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String lastAccount;
|
||||||
|
private final Boolean nominal;
|
||||||
|
private final String aadharNo;
|
||||||
|
private final String partyName;
|
||||||
|
private final String partyName1;
|
||||||
|
private final String address1;
|
||||||
|
private final String address2;
|
||||||
|
private final String address3;
|
||||||
|
private final String city;
|
||||||
|
private final String pincode;
|
||||||
|
private final String gender;
|
||||||
|
private final String panCardNo;
|
||||||
|
private final String telephone;
|
||||||
|
private final String mobileNoSms;
|
||||||
|
private final String operation;
|
||||||
|
private final LocalDate birthDate;
|
||||||
|
private final String occupation;
|
||||||
|
private final String risk;
|
||||||
|
private final String ckycId;
|
||||||
|
private final String adultMinor;
|
||||||
|
private final LocalDate minorDate;
|
||||||
|
private final Long guardianClientId;
|
||||||
|
private final String minAccountOpt;
|
||||||
|
private final String relation;
|
||||||
|
private final LocalDate openingDate;
|
||||||
|
private final Integer month;
|
||||||
|
private final LocalDate maturityDate;
|
||||||
|
private final LocalDate reopenDate;
|
||||||
|
private final BigDecimal depositAmount;
|
||||||
|
private final BigDecimal dailyDepositAmount;
|
||||||
|
private final Integer days;
|
||||||
|
private final String ledgerNo;
|
||||||
|
private final BigDecimal maturityAmount;
|
||||||
|
private final BigDecimal interestRate;
|
||||||
|
private final String withdrawType;
|
||||||
|
private final Boolean od1Flag;
|
||||||
|
private final Boolean od2Flag;
|
||||||
|
private final Boolean lien;
|
||||||
|
private final LocalDate payDate;
|
||||||
|
private final BigDecimal paymentAmount;
|
||||||
|
private final BigDecimal currentBalance;
|
||||||
|
private final BigDecimal openingBalance;
|
||||||
|
private final Boolean isSmsActive;
|
||||||
|
private final Boolean isEmailActive;
|
||||||
|
private final BigDecimal interestCalc;
|
||||||
|
private final LocalDate interestDate;
|
||||||
|
private final String spDescription;
|
||||||
|
private final String freezeCode;
|
||||||
|
private final String advCode;
|
||||||
|
private final String creditToBranch;
|
||||||
|
private final String creditToAccountType;
|
||||||
|
private final String creditToAccount;
|
||||||
|
private final Boolean nomineeApplicable;
|
||||||
|
private final String nomineeSplitMode;
|
||||||
|
private final String status;
|
||||||
|
private final LocalDate closeDate;
|
||||||
|
private final Collection<PartyNomineeData> nominees;
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.party.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public final class PartyNomineeData implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final LocalDate nominationDate;
|
||||||
|
private final String name;
|
||||||
|
private final String mobileNo;
|
||||||
|
private final LocalDate birthDate;
|
||||||
|
private final Boolean minor;
|
||||||
|
private final String relation;
|
||||||
|
private final String gender;
|
||||||
|
private final String guardianName;
|
||||||
|
|
||||||
|
public static PartyNomineeData instance(final Long id, final LocalDate nominationDate, final String name, final String mobileNo,
|
||||||
|
final LocalDate birthDate, final Boolean minor, final String relation, final String gender, final String guardianName) {
|
||||||
|
return new PartyNomineeData(id, nominationDate, name, mobileNo, birthDate, minor, relation, gender, guardianName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PartyNomineeData(final Long id, final LocalDate nominationDate, final String name, final String mobileNo,
|
||||||
|
final LocalDate birthDate, final Boolean minor, final String relation, final String gender, final String guardianName) {
|
||||||
|
this.id = id;
|
||||||
|
this.nominationDate = nominationDate;
|
||||||
|
this.name = name;
|
||||||
|
this.mobileNo = mobileNo;
|
||||||
|
this.birthDate = birthDate;
|
||||||
|
this.minor = minor;
|
||||||
|
this.relation = relation;
|
||||||
|
this.gender = gender;
|
||||||
|
this.guardianName = guardianName;
|
||||||
|
}
|
||||||
|
}
|
||||||
+569
@@ -0,0 +1,569 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.party.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.CascadeType;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OrderBy;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DDS Party - the customer's daily-deposit scheme account, linked to the Agent who collects for it.
|
||||||
|
* Carries the scheme's KYC, terms and two-stage overdue flags, plus a nominee list.
|
||||||
|
*
|
||||||
|
* The underlying RD (recurring deposit) account this scheme conceptually reuses is not wired up yet -
|
||||||
|
* {@code savingsAccountId} is a nullable placeholder for that future linkage (see the DDS architecture
|
||||||
|
* decision to reuse the RD engine for the deposit half). Until then the scheme/balance fields below
|
||||||
|
* are this record's own, not mirrored from any m_savings_account row.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_party", uniqueConstraints = { @UniqueConstraint(columnNames = { "party_code" }, name = "party_code_UNIQUE") })
|
||||||
|
public class Party extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
/** DDS accounts carry a flat interest rate unless the officer explicitly overrides it at creation. */
|
||||||
|
private static final BigDecimal DEFAULT_INTEREST_RATE = new BigDecimal("2");
|
||||||
|
|
||||||
|
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||||
|
public static final String STATUS_CLOSED = "CLOSED";
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "agent_id", nullable = false)
|
||||||
|
private Agent agent;
|
||||||
|
|
||||||
|
@Column(name = "savings_account_id")
|
||||||
|
private Long savingsAccountId;
|
||||||
|
|
||||||
|
@Column(name = "sub_type", length = 60, nullable = false)
|
||||||
|
private String subType;
|
||||||
|
|
||||||
|
@Column(name = "party_code", length = 20, nullable = false)
|
||||||
|
private String partyCode;
|
||||||
|
|
||||||
|
@Column(name = "last_account", length = 50)
|
||||||
|
private String lastAccount;
|
||||||
|
|
||||||
|
@Column(name = "nominal", nullable = false)
|
||||||
|
private boolean nominal;
|
||||||
|
|
||||||
|
@Column(name = "aadhar_no", length = 20)
|
||||||
|
private String aadharNo;
|
||||||
|
|
||||||
|
@Column(name = "party_name", length = 100, nullable = false)
|
||||||
|
private String partyName;
|
||||||
|
|
||||||
|
@Column(name = "party_name1", length = 100)
|
||||||
|
private String partyName1;
|
||||||
|
|
||||||
|
@Column(name = "address1", length = 200, nullable = false)
|
||||||
|
private String address1;
|
||||||
|
|
||||||
|
@Column(name = "address2", length = 200)
|
||||||
|
private String address2;
|
||||||
|
|
||||||
|
@Column(name = "address3", length = 200)
|
||||||
|
private String address3;
|
||||||
|
|
||||||
|
@Column(name = "city", length = 100, nullable = false)
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@Column(name = "pincode", length = 20, nullable = false)
|
||||||
|
private String pincode;
|
||||||
|
|
||||||
|
@Column(name = "gender", length = 20)
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
@Column(name = "pan_card_no", length = 20)
|
||||||
|
private String panCardNo;
|
||||||
|
|
||||||
|
@Column(name = "telephone", length = 20)
|
||||||
|
private String telephone;
|
||||||
|
|
||||||
|
@Column(name = "mobile_no_sms", length = 20, nullable = false)
|
||||||
|
private String mobileNoSms;
|
||||||
|
|
||||||
|
@Column(name = "operation", length = 20, nullable = false)
|
||||||
|
private String operation;
|
||||||
|
|
||||||
|
@Column(name = "birth_date")
|
||||||
|
private LocalDate birthDate;
|
||||||
|
|
||||||
|
@Column(name = "occupation", length = 40)
|
||||||
|
private String occupation;
|
||||||
|
|
||||||
|
@Column(name = "risk", length = 20)
|
||||||
|
private String risk;
|
||||||
|
|
||||||
|
@Column(name = "ckyc_id", length = 40)
|
||||||
|
private String ckycId;
|
||||||
|
|
||||||
|
@Column(name = "adult_minor", length = 20, nullable = false)
|
||||||
|
private String adultMinor;
|
||||||
|
|
||||||
|
@Column(name = "minor_date")
|
||||||
|
private LocalDate minorDate;
|
||||||
|
|
||||||
|
@Column(name = "guardian_client_id")
|
||||||
|
private Long guardianClientId;
|
||||||
|
|
||||||
|
@Column(name = "min_account_opt", length = 40)
|
||||||
|
private String minAccountOpt;
|
||||||
|
|
||||||
|
@Column(name = "relation", length = 40)
|
||||||
|
private String relation;
|
||||||
|
|
||||||
|
@Column(name = "opening_date", nullable = false)
|
||||||
|
private LocalDate openingDate;
|
||||||
|
|
||||||
|
@Column(name = "months")
|
||||||
|
private Integer month;
|
||||||
|
|
||||||
|
@Column(name = "maturity_date")
|
||||||
|
private LocalDate maturityDate;
|
||||||
|
|
||||||
|
@Column(name = "reopen_date")
|
||||||
|
private LocalDate reopenDate;
|
||||||
|
|
||||||
|
@Column(name = "deposit_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal depositAmount;
|
||||||
|
|
||||||
|
@Column(name = "daily_deposit_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal dailyDepositAmount;
|
||||||
|
|
||||||
|
@Column(name = "days", nullable = false)
|
||||||
|
private Integer days;
|
||||||
|
|
||||||
|
@Column(name = "ledger_no", length = 40)
|
||||||
|
private String ledgerNo;
|
||||||
|
|
||||||
|
@Column(name = "maturity_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal maturityAmount;
|
||||||
|
|
||||||
|
@Column(name = "interest_rate", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal interestRate;
|
||||||
|
|
||||||
|
@Column(name = "withdraw_type", length = 20)
|
||||||
|
private String withdrawType;
|
||||||
|
|
||||||
|
@Column(name = "od1_flag", nullable = false)
|
||||||
|
private boolean od1Flag;
|
||||||
|
|
||||||
|
@Column(name = "od2_flag", nullable = false)
|
||||||
|
private boolean od2Flag;
|
||||||
|
|
||||||
|
@Column(name = "lien", nullable = false)
|
||||||
|
private boolean lien;
|
||||||
|
|
||||||
|
@Column(name = "pay_date")
|
||||||
|
private LocalDate payDate;
|
||||||
|
|
||||||
|
@Column(name = "payment_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal paymentAmount;
|
||||||
|
|
||||||
|
@Column(name = "current_balance", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal currentBalance;
|
||||||
|
|
||||||
|
@Column(name = "opening_balance", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal openingBalance;
|
||||||
|
|
||||||
|
@Column(name = "is_sms_active", nullable = false)
|
||||||
|
private boolean isSmsActive;
|
||||||
|
|
||||||
|
@Column(name = "is_email_active", nullable = false)
|
||||||
|
private boolean isEmailActive;
|
||||||
|
|
||||||
|
@Column(name = "interest_calc", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal interestCalc;
|
||||||
|
|
||||||
|
@Column(name = "interest_date")
|
||||||
|
private LocalDate interestDate;
|
||||||
|
|
||||||
|
@Column(name = "sp_description", length = 500)
|
||||||
|
private String spDescription;
|
||||||
|
|
||||||
|
@Column(name = "freeze_code", length = 20, nullable = false)
|
||||||
|
private String freezeCode;
|
||||||
|
|
||||||
|
@Column(name = "adv_code", length = 40, nullable = false)
|
||||||
|
private String advCode;
|
||||||
|
|
||||||
|
@Column(name = "credit_to_branch", length = 100)
|
||||||
|
private String creditToBranch;
|
||||||
|
|
||||||
|
@Column(name = "credit_to_account_type", length = 100)
|
||||||
|
private String creditToAccountType;
|
||||||
|
|
||||||
|
@Column(name = "credit_to_account", length = 50)
|
||||||
|
private String creditToAccount;
|
||||||
|
|
||||||
|
@Column(name = "nominee_applicable", nullable = false)
|
||||||
|
private boolean nomineeApplicable;
|
||||||
|
|
||||||
|
@Column(name = "nominee_split_mode", length = 20)
|
||||||
|
private String nomineeSplitMode;
|
||||||
|
|
||||||
|
@Column(name = "status", length = 20, nullable = false)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "close_date")
|
||||||
|
private LocalDate closeDate;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "party", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
|
@OrderBy("id")
|
||||||
|
private List<PartyNominee> nominees = new ArrayList<>();
|
||||||
|
|
||||||
|
protected Party() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
private Party(final Client client, final Agent agent, final String partyCode, final JsonCommand command) {
|
||||||
|
this.client = client;
|
||||||
|
this.agent = agent;
|
||||||
|
this.partyCode = partyCode;
|
||||||
|
this.subType = command.stringValueOfParameterNamed("subType");
|
||||||
|
this.lastAccount = command.stringValueOfParameterNamedAllowingNull("lastAccount");
|
||||||
|
this.nominal = command.booleanPrimitiveValueOfParameterNamed("nominal");
|
||||||
|
this.aadharNo = command.stringValueOfParameterNamedAllowingNull("aadharNo");
|
||||||
|
this.partyName = command.stringValueOfParameterNamed("partyName");
|
||||||
|
this.partyName1 = command.stringValueOfParameterNamedAllowingNull("partyName1");
|
||||||
|
this.address1 = command.stringValueOfParameterNamed("address1");
|
||||||
|
this.address2 = command.stringValueOfParameterNamedAllowingNull("address2");
|
||||||
|
this.address3 = command.stringValueOfParameterNamedAllowingNull("address3");
|
||||||
|
this.city = command.stringValueOfParameterNamed("city");
|
||||||
|
this.pincode = command.stringValueOfParameterNamed("pincode");
|
||||||
|
this.gender = command.stringValueOfParameterNamedAllowingNull("gender");
|
||||||
|
this.panCardNo = command.stringValueOfParameterNamedAllowingNull("panCardNo");
|
||||||
|
this.telephone = command.stringValueOfParameterNamedAllowingNull("telephone");
|
||||||
|
this.mobileNoSms = command.stringValueOfParameterNamed("mobileNoSms");
|
||||||
|
this.operation = command.stringValueOfParameterNamed("operation");
|
||||||
|
this.birthDate = command.localDateValueOfParameterNamed("birthDate");
|
||||||
|
this.occupation = command.stringValueOfParameterNamedAllowingNull("occupation");
|
||||||
|
this.risk = command.stringValueOfParameterNamedAllowingNull("risk");
|
||||||
|
this.ckycId = command.stringValueOfParameterNamedAllowingNull("ckycId");
|
||||||
|
this.adultMinor = command.stringValueOfParameterNamed("adultMinor");
|
||||||
|
this.minorDate = command.localDateValueOfParameterNamed("minorDate");
|
||||||
|
this.guardianClientId = command.longValueOfParameterNamed("guardianClientId");
|
||||||
|
this.minAccountOpt = command.stringValueOfParameterNamedAllowingNull("minAccountOpt");
|
||||||
|
this.relation = command.stringValueOfParameterNamedAllowingNull("relation");
|
||||||
|
this.openingDate = command.localDateValueOfParameterNamed("openingDate");
|
||||||
|
this.month = command.integerValueOfParameterNamedDefaultToNullIfZero("month");
|
||||||
|
this.maturityDate = command.localDateValueOfParameterNamed("maturityDate");
|
||||||
|
this.reopenDate = command.localDateValueOfParameterNamed("reopenDate");
|
||||||
|
this.depositAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("depositAmount"));
|
||||||
|
this.dailyDepositAmount = command.bigDecimalValueOfParameterNamed("dailyDepositAmount");
|
||||||
|
this.days = command.integerValueOfParameterNamed("days");
|
||||||
|
this.ledgerNo = command.stringValueOfParameterNamedAllowingNull("ledgerNo");
|
||||||
|
this.maturityAmount = BigDecimal.ZERO;
|
||||||
|
this.interestRate = command.parameterExists("interestRate")
|
||||||
|
? command.bigDecimalValueOfParameterNamed("interestRate")
|
||||||
|
: DEFAULT_INTEREST_RATE;
|
||||||
|
this.withdrawType = command.stringValueOfParameterNamedAllowingNull("withdrawType");
|
||||||
|
this.od1Flag = false;
|
||||||
|
this.od2Flag = false;
|
||||||
|
this.lien = command.booleanPrimitiveValueOfParameterNamed("lien");
|
||||||
|
this.payDate = command.localDateValueOfParameterNamed("payDate");
|
||||||
|
this.paymentAmount = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("paymentAmount"));
|
||||||
|
this.openingBalance = orZero(command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("openingBalance"));
|
||||||
|
this.currentBalance = this.openingBalance;
|
||||||
|
this.isSmsActive = command.booleanPrimitiveValueOfParameterNamed("isSmsActive");
|
||||||
|
this.isEmailActive = command.booleanPrimitiveValueOfParameterNamed("isEmailActive");
|
||||||
|
this.interestCalc = BigDecimal.ZERO;
|
||||||
|
this.interestDate = command.localDateValueOfParameterNamed("interestDate");
|
||||||
|
this.spDescription = command.stringValueOfParameterNamedAllowingNull("spDescription");
|
||||||
|
this.freezeCode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("freezeCode"), "Normal");
|
||||||
|
this.advCode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("advCode"), "None");
|
||||||
|
this.creditToBranch = command.stringValueOfParameterNamedAllowingNull("creditToBranch");
|
||||||
|
this.creditToAccountType = command.stringValueOfParameterNamedAllowingNull("creditToAccountType");
|
||||||
|
this.creditToAccount = command.stringValueOfParameterNamedAllowingNull("creditToAccount");
|
||||||
|
this.nomineeApplicable = command.booleanPrimitiveValueOfParameterNamed("nomineeApplicable");
|
||||||
|
this.nomineeSplitMode = StringUtils.defaultIfBlank(command.stringValueOfParameterNamedAllowingNull("nomineeSplitMode"),
|
||||||
|
"Percentage");
|
||||||
|
this.status = STATUS_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Party fromJson(final Client client, final Agent agent, final String partyCode, final JsonCommand command) {
|
||||||
|
return new Party(client, agent, partyCode, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal orZero(final BigDecimal value) {
|
||||||
|
return value == null ? BigDecimal.ZERO : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNominee(final PartyNominee nominee) {
|
||||||
|
nominee.setParty(this);
|
||||||
|
this.nominees.add(nominee);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetNominees() {
|
||||||
|
this.nominees.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeClient(final Client newClient) {
|
||||||
|
this.client = newClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeAgent(final Agent newAgent) {
|
||||||
|
this.agent = newAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Links this Party to an existing real Savings Account, resolved by account number in the write
|
||||||
|
* service. There is no unlink - once a Party's DDS scheme is tied to a real account, it stays
|
||||||
|
* tied.
|
||||||
|
*/
|
||||||
|
public void linkSavingsAccount(final Long savingsAccountId) {
|
||||||
|
this.savingsAccountId = savingsAccountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal credit(final BigDecimal amount) {
|
||||||
|
this.currentBalance = this.currentBalance.add(amount);
|
||||||
|
return this.currentBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal debit(final BigDecimal amount) {
|
||||||
|
this.currentBalance = this.currentBalance.subtract(amount);
|
||||||
|
return this.currentBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isClosed() {
|
||||||
|
return STATUS_CLOSED.equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes this Party's account as of the given date. The balance must already be zero - a Party
|
||||||
|
* is settled to zero (a full Withdraw Deposite/payout) before it is closed, not closed with money
|
||||||
|
* still owed to it.
|
||||||
|
*/
|
||||||
|
public void close(final LocalDate closeDate) {
|
||||||
|
this.status = STATUS_CLOSED;
|
||||||
|
this.closeDate = closeDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stamps the date interest (regular or OD) was last calculated up to, so the next batch run
|
||||||
|
* accrues only the days since. Set on POST, not on calculate/preview.
|
||||||
|
*/
|
||||||
|
public void markInterestCalculatedThrough(final LocalDate date) {
|
||||||
|
this.interestDate = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long clientId() {
|
||||||
|
return this.client.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long agentId() {
|
||||||
|
return this.agent.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> update(final JsonCommand command) {
|
||||||
|
|
||||||
|
final Map<String, Object> actualChanges = new LinkedHashMap<>(40);
|
||||||
|
|
||||||
|
if (command.isChangeInLongParameterNamed("clientId", this.client.getId())) {
|
||||||
|
actualChanges.put("clientId", command.longValueOfParameterNamed("clientId"));
|
||||||
|
}
|
||||||
|
if (command.isChangeInLongParameterNamed("agentId", this.agent.getId())) {
|
||||||
|
actualChanges.put("agentId", command.longValueOfParameterNamed("agentId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "subType", this.subType, v -> this.subType = v);
|
||||||
|
stringChange(command, actualChanges, "lastAccount", this.lastAccount, v -> this.lastAccount = v);
|
||||||
|
|
||||||
|
if (command.isChangeInBooleanParameterNamed("nominal", this.nominal)) {
|
||||||
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("nominal");
|
||||||
|
actualChanges.put("nominal", newValue);
|
||||||
|
this.nominal = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "aadharNo", this.aadharNo, v -> this.aadharNo = v);
|
||||||
|
stringChange(command, actualChanges, "partyName", this.partyName, v -> this.partyName = v);
|
||||||
|
stringChange(command, actualChanges, "partyName1", this.partyName1, v -> this.partyName1 = v);
|
||||||
|
stringChange(command, actualChanges, "address1", this.address1, v -> this.address1 = v);
|
||||||
|
stringChange(command, actualChanges, "address2", this.address2, v -> this.address2 = v);
|
||||||
|
stringChange(command, actualChanges, "address3", this.address3, v -> this.address3 = v);
|
||||||
|
stringChange(command, actualChanges, "city", this.city, v -> this.city = v);
|
||||||
|
stringChange(command, actualChanges, "pincode", this.pincode, v -> this.pincode = v);
|
||||||
|
stringChange(command, actualChanges, "gender", this.gender, v -> this.gender = v);
|
||||||
|
stringChange(command, actualChanges, "panCardNo", this.panCardNo, v -> this.panCardNo = v);
|
||||||
|
stringChange(command, actualChanges, "telephone", this.telephone, v -> this.telephone = v);
|
||||||
|
stringChange(command, actualChanges, "mobileNoSms", this.mobileNoSms, v -> this.mobileNoSms = v);
|
||||||
|
stringChange(command, actualChanges, "operation", this.operation, v -> this.operation = v);
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("birthDate", this.birthDate)) {
|
||||||
|
actualChanges.put("birthDate", command.stringValueOfParameterNamed("birthDate"));
|
||||||
|
this.birthDate = command.localDateValueOfParameterNamed("birthDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "occupation", this.occupation, v -> this.occupation = v);
|
||||||
|
stringChange(command, actualChanges, "risk", this.risk, v -> this.risk = v);
|
||||||
|
stringChange(command, actualChanges, "ckycId", this.ckycId, v -> this.ckycId = v);
|
||||||
|
stringChange(command, actualChanges, "adultMinor", this.adultMinor, v -> this.adultMinor = v);
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("minorDate", this.minorDate)) {
|
||||||
|
actualChanges.put("minorDate", command.stringValueOfParameterNamed("minorDate"));
|
||||||
|
this.minorDate = command.localDateValueOfParameterNamed("minorDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInLongParameterNamed("guardianClientId", this.guardianClientId)) {
|
||||||
|
final Long newValue = command.longValueOfParameterNamed("guardianClientId");
|
||||||
|
actualChanges.put("guardianClientId", newValue);
|
||||||
|
this.guardianClientId = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "minAccountOpt", this.minAccountOpt, v -> this.minAccountOpt = v);
|
||||||
|
stringChange(command, actualChanges, "relation", this.relation, v -> this.relation = v);
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("openingDate", this.openingDate)) {
|
||||||
|
actualChanges.put("openingDate", command.stringValueOfParameterNamed("openingDate"));
|
||||||
|
this.openingDate = command.localDateValueOfParameterNamed("openingDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInIntegerParameterNamed("month", this.month)) {
|
||||||
|
final Integer newValue = command.integerValueOfParameterNamed("month");
|
||||||
|
actualChanges.put("month", newValue);
|
||||||
|
this.month = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("maturityDate", this.maturityDate)) {
|
||||||
|
actualChanges.put("maturityDate", command.stringValueOfParameterNamed("maturityDate"));
|
||||||
|
this.maturityDate = command.localDateValueOfParameterNamed("maturityDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("reopenDate", this.reopenDate)) {
|
||||||
|
actualChanges.put("reopenDate", command.stringValueOfParameterNamed("reopenDate"));
|
||||||
|
this.reopenDate = command.localDateValueOfParameterNamed("reopenDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("depositAmount", this.depositAmount)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("depositAmount");
|
||||||
|
actualChanges.put("depositAmount", newValue);
|
||||||
|
this.depositAmount = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("dailyDepositAmount", this.dailyDepositAmount)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("dailyDepositAmount");
|
||||||
|
actualChanges.put("dailyDepositAmount", newValue);
|
||||||
|
this.dailyDepositAmount = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInIntegerParameterNamed("days", this.days)) {
|
||||||
|
final Integer newValue = command.integerValueOfParameterNamed("days");
|
||||||
|
actualChanges.put("days", newValue);
|
||||||
|
this.days = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("interestRate", this.interestRate)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("interestRate");
|
||||||
|
actualChanges.put("interestRate", newValue);
|
||||||
|
this.interestRate = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "ledgerNo", this.ledgerNo, v -> this.ledgerNo = v);
|
||||||
|
stringChange(command, actualChanges, "withdrawType", this.withdrawType, v -> this.withdrawType = v);
|
||||||
|
|
||||||
|
if (command.isChangeInBooleanParameterNamed("lien", this.lien)) {
|
||||||
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("lien");
|
||||||
|
actualChanges.put("lien", newValue);
|
||||||
|
this.lien = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("payDate", this.payDate)) {
|
||||||
|
actualChanges.put("payDate", command.stringValueOfParameterNamed("payDate"));
|
||||||
|
this.payDate = command.localDateValueOfParameterNamed("payDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("paymentAmount", this.paymentAmount)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("paymentAmount");
|
||||||
|
actualChanges.put("paymentAmount", newValue);
|
||||||
|
this.paymentAmount = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBigDecimalParameterNamed("openingBalance", this.openingBalance)) {
|
||||||
|
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed("openingBalance");
|
||||||
|
actualChanges.put("openingBalance", newValue);
|
||||||
|
this.openingBalance = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBooleanParameterNamed("isSmsActive", this.isSmsActive)) {
|
||||||
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("isSmsActive");
|
||||||
|
actualChanges.put("isSmsActive", newValue);
|
||||||
|
this.isSmsActive = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInBooleanParameterNamed("isEmailActive", this.isEmailActive)) {
|
||||||
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("isEmailActive");
|
||||||
|
actualChanges.put("isEmailActive", newValue);
|
||||||
|
this.isEmailActive = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInDateParameterNamed("interestDate", this.interestDate)) {
|
||||||
|
actualChanges.put("interestDate", command.stringValueOfParameterNamed("interestDate"));
|
||||||
|
this.interestDate = command.localDateValueOfParameterNamed("interestDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "spDescription", this.spDescription, v -> this.spDescription = v);
|
||||||
|
stringChange(command, actualChanges, "freezeCode", this.freezeCode, v -> this.freezeCode = v);
|
||||||
|
stringChange(command, actualChanges, "advCode", this.advCode, v -> this.advCode = v);
|
||||||
|
stringChange(command, actualChanges, "creditToBranch", this.creditToBranch, v -> this.creditToBranch = v);
|
||||||
|
stringChange(command, actualChanges, "creditToAccountType", this.creditToAccountType, v -> this.creditToAccountType = v);
|
||||||
|
stringChange(command, actualChanges, "creditToAccount", this.creditToAccount, v -> this.creditToAccount = v);
|
||||||
|
|
||||||
|
if (command.isChangeInBooleanParameterNamed("nomineeApplicable", this.nomineeApplicable)) {
|
||||||
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("nomineeApplicable");
|
||||||
|
actualChanges.put("nomineeApplicable", newValue);
|
||||||
|
this.nomineeApplicable = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stringChange(command, actualChanges, "nomineeSplitMode", this.nomineeSplitMode, v -> this.nomineeSplitMode = v);
|
||||||
|
|
||||||
|
return actualChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stringChange(final JsonCommand command, final Map<String, Object> actualChanges, final String paramName,
|
||||||
|
final String existingValue, final Consumer<String> setter) {
|
||||||
|
if (command.isChangeInStringParameterNamed(paramName, existingValue)) {
|
||||||
|
final String newValue = command.stringValueOfParameterNamed(paramName);
|
||||||
|
actualChanges.put(paramName, newValue);
|
||||||
|
setter.accept(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.party.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 lombok.Getter;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single nominee recorded against a {@link Party}. Maker-checker on nominee edits is enforced at
|
||||||
|
* the command layer (the whole Party update goes through the standard maker-checker command flow),
|
||||||
|
* not on this child row individually.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_party_nominee")
|
||||||
|
public class PartyNominee extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "party_id", nullable = false)
|
||||||
|
private Party party;
|
||||||
|
|
||||||
|
@Column(name = "nomination_date", nullable = false)
|
||||||
|
private LocalDate nominationDate;
|
||||||
|
|
||||||
|
@Column(name = "name", length = 100, nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "mobile_no", length = 20)
|
||||||
|
private String mobileNo;
|
||||||
|
|
||||||
|
@Column(name = "birth_date")
|
||||||
|
private LocalDate birthDate;
|
||||||
|
|
||||||
|
@Column(name = "minor", nullable = false)
|
||||||
|
private boolean minor;
|
||||||
|
|
||||||
|
@Column(name = "relation", length = 40, nullable = false)
|
||||||
|
private String relation;
|
||||||
|
|
||||||
|
@Column(name = "gender", length = 20, nullable = false)
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
@Column(name = "guardian_name", length = 100)
|
||||||
|
private String guardianName;
|
||||||
|
|
||||||
|
protected PartyNominee() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public PartyNominee(final LocalDate nominationDate, final String name, final String mobileNo, final LocalDate birthDate,
|
||||||
|
final boolean minor, final String relation, final String gender, final String guardianName) {
|
||||||
|
this.nominationDate = nominationDate;
|
||||||
|
this.name = name;
|
||||||
|
this.mobileNo = StringUtils.defaultIfEmpty(mobileNo, null);
|
||||||
|
this.birthDate = birthDate;
|
||||||
|
this.minor = minor;
|
||||||
|
this.relation = relation;
|
||||||
|
this.gender = gender;
|
||||||
|
this.guardianName = StringUtils.defaultIfEmpty(guardianName, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setParty(final Party party) {
|
||||||
|
this.party = party;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-5
@@ -16,13 +16,17 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.exception;
|
package org.apache.fineract.portfolio.dds.party.exception;
|
||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
public class FacilityChargeMappingNotFoundException extends AbstractPlatformResourceNotFoundException {
|
/**
|
||||||
|
* Thrown when a Close Party command, or a Party Tran collection, targets a Party that is already
|
||||||
|
* closed.
|
||||||
|
*/
|
||||||
|
public class PartyAlreadyClosedException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
public FacilityChargeMappingNotFoundException(final Long id) {
|
public PartyAlreadyClosedException(final String partyCode) {
|
||||||
super("error.msg.facilitychargemapping.id.invalid", "Facility charge mapping with identifier " + id + " does not exist", id);
|
super("error.msg.party.already.closed", "Party " + partyCode + " is already closed", partyCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.party.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when a Close Party command is attempted while the Party's balance is still non-zero - the
|
||||||
|
* remaining balance must be paid out (a Withdraw Deposite to zero) before the account can be closed.
|
||||||
|
*/
|
||||||
|
public class PartyHasBalanceException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public PartyHasBalanceException(final String partyCode) {
|
||||||
|
super("error.msg.party.has.balance", "Party " + partyCode + " still has a non-zero balance and cannot be closed", partyCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.party.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RuntimeException} thrown when a DDS party resource is not found.
|
||||||
|
*/
|
||||||
|
public class PartyNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public PartyNotFoundException(final Long id) {
|
||||||
|
super("error.msg.party.id.invalid", "Party with identifier " + id + " does not exist", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PartyNotFoundException(final Long id, final EmptyResultDataAccessException e) {
|
||||||
|
super("error.msg.party.id.invalid", "Party with identifier " + id + " does not exist", id, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PartyNotFoundException(final String partyCode) {
|
||||||
|
super("error.msg.party.code.invalid", "Party with code " + partyCode + " does not exist", partyCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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.dds.party.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the savingsAccountNo given for a Party belongs to a different client than the Party's
|
||||||
|
* own clientId - a Party can only be linked to its own client's savings account.
|
||||||
|
*/
|
||||||
|
public class PartySavingsAccountClientMismatchException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public PartySavingsAccountClientMismatchException(final String savingsAccountNo) {
|
||||||
|
super("error.msg.party.savings.account.client.mismatch",
|
||||||
|
"Savings account " + savingsAccountNo + " does not belong to this party's client", savingsAccountNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-14
@@ -16,21 +16,17 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.useradministration.domain;
|
package org.apache.fineract.portfolio.dds.party.exception;
|
||||||
|
|
||||||
public enum UserPendingRequestType {
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
CREATE_USER,
|
|
||||||
UPDATE_USER;
|
|
||||||
|
|
||||||
public static UserPendingRequestType fromString(final String type) {
|
/**
|
||||||
if (type == null) {
|
* Thrown when a Party's savingsAccountNo does not match any non-closed Savings Account.
|
||||||
return null;
|
*/
|
||||||
}
|
public class PartySavingsAccountNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
for (final UserPendingRequestType requestType : values()) {
|
|
||||||
if (requestType.name().equalsIgnoreCase(type)) {
|
public PartySavingsAccountNotFoundException(final String savingsAccountNo) {
|
||||||
return requestType;
|
super("error.msg.party.savings.account.not.found", "No open savings account found with account number " + savingsAccountNo,
|
||||||
}
|
savingsAccountNo);
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Unknown UserPendingRequestType: " + type);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+21
-8
@@ -16,17 +16,30 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.service;
|
package org.apache.fineract.portfolio.dds.report.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.apache.fineract.portfolio.savings.data.FacilityChargeMappingData;
|
import java.util.Map;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
public interface FacilityChargeMappingReadPlatformService {
|
/**
|
||||||
|
* A generic tabular result shape shared by every DDS report - one column list plus one row list, both
|
||||||
|
* keyed the same way, so a single report viewer screen can render any of them without a bespoke
|
||||||
|
* result type per report.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public final class DdsReportData implements Serializable {
|
||||||
|
|
||||||
List<FacilityChargeMappingData> retrieveAll();
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
FacilityChargeMappingData retrieveOne(Long id);
|
private final String reportKey;
|
||||||
|
private final boolean available;
|
||||||
/** All active charges, for the admin screen's "change the mapped charge" dropdown. */
|
private final String message;
|
||||||
FacilityChargeMappingData retrieveTemplate();
|
private final List<String> columns;
|
||||||
|
private final List<Map<String, Object>> rows;
|
||||||
}
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.transaction.data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public final class PartyTransactionData implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long partyId;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String partyName;
|
||||||
|
private final Long agentId;
|
||||||
|
private final String agentCode;
|
||||||
|
private final LocalDate transactionDate;
|
||||||
|
private final String transactionType;
|
||||||
|
private final String direction;
|
||||||
|
private final String transactionMode;
|
||||||
|
private final String accountType;
|
||||||
|
private final String accountNo;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
private final String chequeNo;
|
||||||
|
private final String tokenNo;
|
||||||
|
private final String description;
|
||||||
|
private final BigDecimal balanceAfterTransaction;
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 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.dds.transaction.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.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||||
|
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single posted entry on the DDS ledger: a Party's day's collection ("Party Tran", direction
|
||||||
|
* CREDIT), one leg of a "Withdraw Deposite" transfer between two parties (the creditor leg CREDIT,
|
||||||
|
* the debtor leg DEBIT), or an Agent's settlement of collected cash to its branch ("Agent Tran",
|
||||||
|
* direction DEBIT - no {@link Party} involved, so {@code party} is null for this type).
|
||||||
|
* {@code balanceAfterTransaction} holds the Party's balance for Party-linked rows, or the Agent's
|
||||||
|
* balance for an Agent Tran row. Immutable once posted - like any transaction, it is reversed with
|
||||||
|
* an opposing entry, not edited, so there is deliberately no update API for this entity.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_dds_party_transaction")
|
||||||
|
public class PartyTransaction extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "party_id")
|
||||||
|
private Party party;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "agent_id", nullable = false)
|
||||||
|
private Agent agent;
|
||||||
|
|
||||||
|
@Column(name = "transaction_date", nullable = false)
|
||||||
|
private LocalDate transactionDate;
|
||||||
|
|
||||||
|
@Column(name = "transaction_type", length = 30, nullable = false)
|
||||||
|
private String transactionType;
|
||||||
|
|
||||||
|
@Column(name = "direction", length = 10, nullable = false)
|
||||||
|
private String direction;
|
||||||
|
|
||||||
|
@Column(name = "transaction_mode", length = 20)
|
||||||
|
private String transactionMode;
|
||||||
|
|
||||||
|
@Column(name = "account_type", length = 100)
|
||||||
|
private String accountType;
|
||||||
|
|
||||||
|
@Column(name = "account_no", length = 50)
|
||||||
|
private String accountNo;
|
||||||
|
|
||||||
|
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@Column(name = "cheque_no", length = 40)
|
||||||
|
private String chequeNo;
|
||||||
|
|
||||||
|
@Column(name = "token_no", length = 40)
|
||||||
|
private String tokenNo;
|
||||||
|
|
||||||
|
@Column(name = "description", length = 500)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "balance_after_transaction", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal balanceAfterTransaction;
|
||||||
|
|
||||||
|
protected PartyTransaction() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public PartyTransaction(final Party party, final Agent agent, final LocalDate transactionDate, final String transactionType,
|
||||||
|
final String direction, final String transactionMode, final String accountType, final String accountNo,
|
||||||
|
final BigDecimal amount, final String chequeNo, final String tokenNo, final String description,
|
||||||
|
final BigDecimal balanceAfterTransaction) {
|
||||||
|
this.party = party;
|
||||||
|
this.agent = agent;
|
||||||
|
this.transactionDate = transactionDate;
|
||||||
|
this.transactionType = transactionType;
|
||||||
|
this.direction = direction;
|
||||||
|
this.transactionMode = transactionMode;
|
||||||
|
this.accountType = accountType;
|
||||||
|
this.accountNo = accountNo;
|
||||||
|
this.amount = amount;
|
||||||
|
this.chequeNo = chequeNo;
|
||||||
|
this.tokenNo = tokenNo;
|
||||||
|
this.description = description;
|
||||||
|
this.balanceAfterTransaction = balanceAfterTransaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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.dds.transaction.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when an Agent Tran settlement amount exceeds the Agent's current (collected but not yet
|
||||||
|
* settled) balance.
|
||||||
|
*/
|
||||||
|
public class InsufficientAgentBalanceException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public InsufficientAgentBalanceException(final String agentCode) {
|
||||||
|
super("error.msg.agent.insufficient.balance",
|
||||||
|
"Agent " + agentCode + " does not have sufficient collected balance for this settlement", agentCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-15
@@ -16,22 +16,17 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.useradministration.domain;
|
package org.apache.fineract.portfolio.dds.transaction.exception;
|
||||||
|
|
||||||
public enum UserPendingRequestStatus {
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
PENDING,
|
|
||||||
APPROVED,
|
|
||||||
REJECTED;
|
|
||||||
|
|
||||||
public static UserPendingRequestStatus fromString(final String status) {
|
/**
|
||||||
if (status == null) {
|
* Thrown when a Withdraw Deposite transfer would take the debtor Party's balance below zero.
|
||||||
return null;
|
*/
|
||||||
}
|
public class InsufficientPartyBalanceException extends AbstractPlatformDomainRuleException {
|
||||||
for (final UserPendingRequestStatus requestStatus : values()) {
|
|
||||||
if (requestStatus.name().equalsIgnoreCase(status)) {
|
public InsufficientPartyBalanceException(final String partyCode) {
|
||||||
return requestStatus;
|
super("error.msg.party.insufficient.balance", "Party " + partyCode + " does not have sufficient balance for this transfer",
|
||||||
}
|
partyCode);
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Unknown UserPendingRequestStatus: " + status);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
-13
@@ -16,22 +16,17 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.service;
|
package org.apache.fineract.portfolio.dds.transaction.exception;
|
||||||
|
|
||||||
import java.util.List;
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
import org.apache.fineract.portfolio.savings.data.DormantAccountData;
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the accounts the dormancy job has marked inactive, dormant or escheated.
|
* A {@link RuntimeException} thrown when a DDS party transaction resource is not found.
|
||||||
*/
|
*/
|
||||||
public interface DormantAccountReadPlatformService {
|
public class PartyTransactionNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
/**
|
public PartyTransactionNotFoundException(final Long id, final EmptyResultDataAccessException e) {
|
||||||
* @param subStatus
|
super("error.msg.partytransaction.id.invalid", "Party transaction with identifier " + id + " does not exist", id, e);
|
||||||
* 100 inactive, 200 dormant, 300 escheat; null for all three
|
}
|
||||||
* @param officeName
|
|
||||||
* restricts to one branch; null or blank for every branch
|
|
||||||
* @return the matching accounts, longest idle first
|
|
||||||
*/
|
|
||||||
List<DormantAccountData> retrieveDormantAccounts(Integer subStatus, String officeName);
|
|
||||||
}
|
}
|
||||||
+29
-19
@@ -31,15 +31,20 @@ import jakarta.ws.rs.Produces;
|
|||||||
import jakarta.ws.rs.QueryParam;
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
import org.apache.fineract.command.core.CommandPipeline;
|
||||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
|
||||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||||
|
import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeCreateCommand;
|
||||||
|
import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeDeleteCommand;
|
||||||
|
import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeUpdateCommand;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest;
|
||||||
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
|
||||||
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest;
|
||||||
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest;
|
||||||
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService;
|
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -50,7 +55,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class PaymentTypeApiResource {
|
public class PaymentTypeApiResource {
|
||||||
|
|
||||||
private final PaymentTypeReadService readPlatformService;
|
private final PaymentTypeReadService readPlatformService;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final CommandPipeline commandPipeline;
|
||||||
private final DefaultToApiJsonSerializer<PaymentTypeData> jsonSerializer;
|
private final DefaultToApiJsonSerializer<PaymentTypeData> jsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -74,13 +79,14 @@ public class PaymentTypeApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Create a Payment Type", description = "Creates a new Payment type")
|
@Operation(summary = "Create a Payment Type", description = "Creates a new Payment type")
|
||||||
public String createPaymentType(final PaymentTypeCreateRequest request) {
|
public PaymentTypeCreateResponse createPaymentType(PaymentTypeCreateRequest request) {
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().createPaymentType()
|
final var command = new PaymentTypeCreateCommand();
|
||||||
.withJson(jsonSerializer.serialize(request)).build();
|
|
||||||
|
|
||||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
command.setPayload(request);
|
||||||
|
|
||||||
return jsonSerializer.serialize(result);
|
final Supplier<PaymentTypeCreateResponse> response = commandPipeline.send(command);
|
||||||
|
|
||||||
|
return response.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@@ -88,16 +94,18 @@ public class PaymentTypeApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Update a Payment Type", description = "Updates a Payment Type")
|
@Operation(summary = "Update a Payment Type", description = "Updates a Payment Type")
|
||||||
public String updatePaymentType(@PathParam("paymentTypeId") final Long paymentTypeId, final PaymentTypeUpdateRequest request) {
|
public PaymentTypeUpdateResponse updatePaymentType(@PathParam("paymentTypeId") final Long paymentTypeId,
|
||||||
|
final PaymentTypeUpdateRequest request) {
|
||||||
|
|
||||||
request.setId(paymentTypeId);
|
request.setId(paymentTypeId);
|
||||||
|
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updatePaymentType(paymentTypeId)
|
final var command = new PaymentTypeUpdateCommand();
|
||||||
.withJson(jsonSerializer.serialize(request)).build();
|
|
||||||
|
|
||||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
command.setPayload(request);
|
||||||
|
|
||||||
return jsonSerializer.serialize(result);
|
final Supplier<PaymentTypeUpdateResponse> response = commandPipeline.send(command);
|
||||||
|
|
||||||
|
return response.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@@ -105,12 +113,14 @@ public class PaymentTypeApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Delete a Payment Type", description = "Deletes payment type")
|
@Operation(summary = "Delete a Payment Type", description = "Deletes payment type")
|
||||||
public String deleteCode(@PathParam("paymentTypeId") final Long paymentTypeId) {
|
public PaymentTypeDeleteResponse deleteCode(@PathParam("paymentTypeId") final Long paymentTypeId) {
|
||||||
|
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().deletePaymentType(paymentTypeId).build();
|
final var command = new PaymentTypeDeleteCommand();
|
||||||
|
|
||||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
command.setPayload(PaymentTypeDeleteRequest.builder().id(paymentTypeId).build());
|
||||||
|
|
||||||
return jsonSerializer.serialize(result);
|
final Supplier<PaymentTypeDeleteResponse> response = commandPipeline.send(command);
|
||||||
|
|
||||||
|
return response.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-35
@@ -18,50 +18,33 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.paymenttype.handler;
|
package org.apache.fineract.portfolio.paymenttype.handler;
|
||||||
|
|
||||||
import org.apache.fineract.commands.annotation.CommandType;
|
import io.github.resilience4j.retry.annotation.Retry;
|
||||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.command.core.Command;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.command.core.CommandHandler;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Service
|
@Slf4j
|
||||||
@CommandType(entity = "PAYMENTTYPE", action = "CREATE")
|
@Component
|
||||||
public class PaymentTypeCreateCommandHandler implements NewCommandSourceHandler {
|
@RequiredArgsConstructor
|
||||||
|
public class PaymentTypeCreateCommandHandler implements CommandHandler<PaymentTypeCreateRequest, PaymentTypeCreateResponse> {
|
||||||
|
|
||||||
private final PaymentTypeWriteService paymentTypeWriteService;
|
private final PaymentTypeWriteService paymentTypeWriteService;
|
||||||
|
|
||||||
@Autowired
|
@Retry(name = "commandPaymentTypeCreate", fallbackMethod = "fallback")
|
||||||
public PaymentTypeCreateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
|
|
||||||
this.paymentTypeWriteService = paymentTypeWriteService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
@Transactional
|
||||||
|
public PaymentTypeCreateResponse handle(Command<PaymentTypeCreateRequest> command) {
|
||||||
final Boolean isSystemDefined = command.booleanObjectValueOfParameterNamed("isSystemDefined");
|
return paymentTypeWriteService.createPaymentType(command.getPayload());
|
||||||
|
|
||||||
final PaymentTypeCreateRequest request = PaymentTypeCreateRequest.builder()
|
|
||||||
.name(command.stringValueOfParameterNamed("name"))
|
|
||||||
.description(command.stringValueOfParameterNamed("description"))
|
|
||||||
.isCashPayment(command.booleanObjectValueOfParameterNamed("isCashPayment"))
|
|
||||||
.position(command.longValueOfParameterNamed("position"))
|
|
||||||
.codeName(command.stringValueOfParameterNamed("codeName"))
|
|
||||||
.isSystemDefined(isSystemDefined != null ? isSystemDefined : Boolean.FALSE)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final PaymentTypeCreateResponse response = this.paymentTypeWriteService.createPaymentType(request);
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
|
||||||
.withCommandId(command.commandId()) //
|
|
||||||
.withEntityId(response.getResourceId()) //
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PaymentTypeCreateResponse fallback(Command<PaymentTypeCreateRequest> command, Throwable t) {
|
||||||
|
return CommandHandler.super.fallback(command, t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-26
@@ -18,41 +18,33 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.paymenttype.handler;
|
package org.apache.fineract.portfolio.paymenttype.handler;
|
||||||
|
|
||||||
import org.apache.fineract.commands.annotation.CommandType;
|
import io.github.resilience4j.retry.annotation.Retry;
|
||||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.command.core.Command;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.command.core.CommandHandler;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Service
|
@Slf4j
|
||||||
@CommandType(entity = "PAYMENTTYPE", action = "DELETE")
|
@Component
|
||||||
public class PaymentTypeDeleteCommandHandler implements NewCommandSourceHandler {
|
@RequiredArgsConstructor
|
||||||
|
public class PaymentTypeDeleteCommandHandler implements CommandHandler<PaymentTypeDeleteRequest, PaymentTypeDeleteResponse> {
|
||||||
|
|
||||||
private final PaymentTypeWriteService paymentTypeWriteService;
|
private final PaymentTypeWriteService paymentTypeWriteService;
|
||||||
|
|
||||||
@Autowired
|
@Retry(name = "commandPaymentTypeDelete", fallbackMethod = "fallback")
|
||||||
public PaymentTypeDeleteCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
|
|
||||||
this.paymentTypeWriteService = paymentTypeWriteService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
@Transactional
|
||||||
|
public PaymentTypeDeleteResponse handle(Command<PaymentTypeDeleteRequest> command) {
|
||||||
final PaymentTypeDeleteRequest request = PaymentTypeDeleteRequest.builder().id(command.entityId()).build();
|
return paymentTypeWriteService.deletePaymentType(command.getPayload());
|
||||||
|
|
||||||
final PaymentTypeDeleteResponse response = this.paymentTypeWriteService.deletePaymentType(request);
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
|
||||||
.withCommandId(command.commandId()) //
|
|
||||||
.withEntityId(response.getResourceId()) //
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PaymentTypeDeleteResponse fallback(Command<PaymentTypeDeleteRequest> command, Throwable t) {
|
||||||
|
return CommandHandler.super.fallback(command, t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-36
@@ -18,51 +18,33 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.paymenttype.handler;
|
package org.apache.fineract.portfolio.paymenttype.handler;
|
||||||
|
|
||||||
import org.apache.fineract.commands.annotation.CommandType;
|
import io.github.resilience4j.retry.annotation.Retry;
|
||||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.command.core.Command;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.command.core.CommandHandler;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Service
|
@Slf4j
|
||||||
@CommandType(entity = "PAYMENTTYPE", action = "UPDATE")
|
@Component
|
||||||
public class PaymentTypeUpdateCommandHandler implements NewCommandSourceHandler {
|
@RequiredArgsConstructor
|
||||||
|
public class PaymentTypeUpdateCommandHandler implements CommandHandler<PaymentTypeUpdateRequest, PaymentTypeUpdateResponse> {
|
||||||
|
|
||||||
private final PaymentTypeWriteService paymentTypeWriteService;
|
private final PaymentTypeWriteService paymentTypeWriteService;
|
||||||
|
|
||||||
@Autowired
|
@Retry(name = "commandPaymentTypeUpdate", fallbackMethod = "fallback")
|
||||||
public PaymentTypeUpdateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
|
|
||||||
this.paymentTypeWriteService = paymentTypeWriteService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
@Transactional
|
||||||
|
public PaymentTypeUpdateResponse handle(Command<PaymentTypeUpdateRequest> command) {
|
||||||
final Boolean isSystemDefined = command.booleanObjectValueOfParameterNamed("isSystemDefined");
|
return paymentTypeWriteService.updatePaymentType(command.getPayload());
|
||||||
|
|
||||||
final PaymentTypeUpdateRequest request = PaymentTypeUpdateRequest.builder()
|
|
||||||
.id(command.entityId())
|
|
||||||
.name(command.stringValueOfParameterNamed("name"))
|
|
||||||
.description(command.stringValueOfParameterNamed("description"))
|
|
||||||
.isCashPayment(command.booleanObjectValueOfParameterNamed("isCashPayment"))
|
|
||||||
.position(command.longValueOfParameterNamed("position"))
|
|
||||||
.codeName(command.stringValueOfParameterNamed("codeName"))
|
|
||||||
.isSystemDefined(isSystemDefined != null ? isSystemDefined : Boolean.FALSE)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final PaymentTypeUpdateResponse response = this.paymentTypeWriteService.updatePaymentType(request);
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
|
||||||
.withCommandId(command.commandId()) //
|
|
||||||
.withEntityId(response.getResourceId()) //
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PaymentTypeUpdateResponse fallback(Command<PaymentTypeUpdateRequest> command, Throwable t) {
|
||||||
|
return CommandHandler.super.fallback(command, t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-7
@@ -31,8 +31,7 @@ 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;
|
||||||
@@ -52,7 +51,6 @@ 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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -87,10 +85,6 @@ 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();
|
||||||
|
|||||||
+2
-14
@@ -123,7 +123,6 @@ public final class DepositsApiConstants {
|
|||||||
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
||||||
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
|
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
|
||||||
public static final String tenureInMonthsParamName = "tenureInMonths";
|
public static final String tenureInMonthsParamName = "tenureInMonths";
|
||||||
public static final String tenureInDaysParamName = "tenureInDays";
|
|
||||||
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
||||||
|
|
||||||
// Preclosure parameters
|
// Preclosure parameters
|
||||||
@@ -143,9 +142,6 @@ public final class DepositsApiConstants {
|
|||||||
public static final String inMultiplesOfDepositTermParamName = "inMultiplesOfDepositTerm";
|
public static final String inMultiplesOfDepositTermParamName = "inMultiplesOfDepositTerm";
|
||||||
public static final String inMultiplesOfDepositTermTypeIdParamName = "inMultiplesOfDepositTermTypeId";
|
public static final String inMultiplesOfDepositTermTypeIdParamName = "inMultiplesOfDepositTermTypeId";
|
||||||
public static final String inMultiplesOfDepositTermType = "inMultiplesOfDepositTermType";
|
public static final String inMultiplesOfDepositTermType = "inMultiplesOfDepositTermType";
|
||||||
// Days after maturity a depositor still has to walk in and renew at the original maturity date --
|
|
||||||
// null/0 means this product keeps the instant, same-night auto-renewal every FD product had before.
|
|
||||||
public static final String renewalGracePeriodDaysParamName = "renewalGracePeriodDays";
|
|
||||||
|
|
||||||
public static final String depositAmountParamName = "depositAmount";
|
public static final String depositAmountParamName = "depositAmount";
|
||||||
public static final String depositMinAmountParamName = "minDepositAmount";
|
public static final String depositMinAmountParamName = "minDepositAmount";
|
||||||
@@ -212,7 +208,6 @@ 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";
|
||||||
@@ -251,13 +246,11 @@ public final class DepositsApiConstants {
|
|||||||
|
|
||||||
private static final Set<String> DEPOSIT_TERM_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
|
private static final Set<String> DEPOSIT_TERM_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
|
||||||
maxDepositTermParamName, minDepositTermTypeIdParamName, maxDepositTermTypeIdParamName, inMultiplesOfDepositTermParamName,
|
maxDepositTermParamName, minDepositTermTypeIdParamName, maxDepositTermTypeIdParamName, inMultiplesOfDepositTermParamName,
|
||||||
inMultiplesOfDepositTermTypeIdParamName, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName,
|
inMultiplesOfDepositTermTypeIdParamName, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName));
|
||||||
renewalGracePeriodDaysParamName));
|
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
|
private static final Set<String> DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
|
||||||
maxDepositTermParamName, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTermParamName,
|
maxDepositTermParamName, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTermParamName,
|
||||||
inMultiplesOfDepositTermType, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName,
|
inMultiplesOfDepositTermType, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName));
|
||||||
renewalGracePeriodDaysParamName));
|
|
||||||
|
|
||||||
private static final Set<String> RECURRING_DETAILS_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> RECURRING_DETAILS_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName,
|
Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName,
|
||||||
@@ -517,7 +510,6 @@ public final class DepositsApiConstants {
|
|||||||
fixedDepositInterestCalculationData.add(principalAmountParamName);
|
fixedDepositInterestCalculationData.add(principalAmountParamName);
|
||||||
fixedDepositInterestCalculationData.add(annualInterestRateParamName);
|
fixedDepositInterestCalculationData.add(annualInterestRateParamName);
|
||||||
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
|
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
|
||||||
fixedDepositInterestCalculationData.add(tenureInDaysParamName);
|
|
||||||
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
|
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
|
||||||
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
|
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
|
||||||
// Exact calendar-based calculation params
|
// Exact calendar-based calculation params
|
||||||
@@ -596,10 +588,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-13
@@ -92,9 +92,6 @@ 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";
|
||||||
@@ -229,7 +226,6 @@ 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";
|
||||||
@@ -258,6 +254,7 @@ 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";
|
||||||
|
|
||||||
@@ -278,6 +275,7 @@ public class SavingsApiConstants {
|
|||||||
// Additional custom fields
|
// Additional custom fields
|
||||||
public static final String balanceFrequencyParamName = "balanceFrequency";
|
public static final String balanceFrequencyParamName = "balanceFrequency";
|
||||||
public static final String accountClosedZeroBalanceInDayClosingParamName = "accountClosedZeroBalanceInDayClosing";
|
public static final String accountClosedZeroBalanceInDayClosingParamName = "accountClosedZeroBalanceInDayClosing";
|
||||||
|
public static final String productCategoryParamName = "productCategory";
|
||||||
|
|
||||||
// Current Product — new UI fields
|
// Current Product — new UI fields
|
||||||
public static final String balanceSwapOptionParamName = "balanceSwapOption";
|
public static final String balanceSwapOptionParamName = "balanceSwapOption";
|
||||||
@@ -286,15 +284,6 @@ public class SavingsApiConstants {
|
|||||||
public static final String avgBalanceCalcPeriodParamName = "avgBalanceCalcPeriod";
|
public static final String avgBalanceCalcPeriodParamName = "avgBalanceCalcPeriod";
|
||||||
public static final String validForPeriodParamName = "validForPeriod";
|
public static final String validForPeriodParamName = "validForPeriod";
|
||||||
public static final String odAccountClosedZeroBalanceInDayClosingParamName = "odAccountClosedZeroBalanceInDayClosing";
|
public static final String odAccountClosedZeroBalanceInDayClosingParamName = "odAccountClosedZeroBalanceInDayClosing";
|
||||||
// Overdraft scheme rules a sanction on an account is held to: overdraftLimit is the ceiling (an
|
|
||||||
// amount), odMinLimit the floor, odSecurityType whether the limit may be clean, secured, or either.
|
|
||||||
public static final String odMinLimitParamName = "odMinLimit";
|
|
||||||
public static final String odSecurityTypeParamName = "odSecurityType";
|
|
||||||
// Excess drawing: whether an account may go past its sanctioned limit, by how much (a percentage of the
|
|
||||||
// limit), and the penal charge (% p.a.) levied on the excess alone for the days it stays excess.
|
|
||||||
public static final String odExcessAllowedParamName = "odExcessAllowed";
|
|
||||||
public static final String odExcessLimitPercentParamName = "odExcessLimitPercent";
|
|
||||||
public static final String odPenalRateParamName = "odPenalRate";
|
|
||||||
|
|
||||||
// Status of Account fields
|
// Status of Account fields
|
||||||
public static final String accountStatusParamName = "accountStatus";
|
public static final String accountStatusParamName = "accountStatus";
|
||||||
|
|||||||
-17
@@ -157,8 +157,6 @@ 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;
|
||||||
@@ -219,21 +217,6 @@ 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,
|
||||||
|
|||||||
+1
-10
@@ -144,6 +144,7 @@ public final class SavingsProductData implements Serializable {
|
|||||||
@Getter @Setter private Boolean enableLockinPeriod;
|
@Getter @Setter private Boolean enableLockinPeriod;
|
||||||
@Getter @Setter private String balanceFrequency;
|
@Getter @Setter private String balanceFrequency;
|
||||||
@Getter @Setter private Boolean accountClosedZeroBalanceInDayClosing;
|
@Getter @Setter private Boolean accountClosedZeroBalanceInDayClosing;
|
||||||
|
@Getter @Setter private String productCategory;
|
||||||
|
|
||||||
// New current product fields
|
// New current product fields
|
||||||
@Getter @Setter private Boolean balanceSwapOption;
|
@Getter @Setter private Boolean balanceSwapOption;
|
||||||
@@ -152,11 +153,6 @@ public final class SavingsProductData implements Serializable {
|
|||||||
@Getter @Setter private String avgBalanceCalcPeriod;
|
@Getter @Setter private String avgBalanceCalcPeriod;
|
||||||
@Getter @Setter private String validForPeriod;
|
@Getter @Setter private String validForPeriod;
|
||||||
@Getter @Setter private Boolean odAccountClosedZeroBalanceInDayClosing;
|
@Getter @Setter private Boolean odAccountClosedZeroBalanceInDayClosing;
|
||||||
@Getter @Setter private BigDecimal odMinLimit;
|
|
||||||
@Getter @Setter private String odSecurityType;
|
|
||||||
@Getter @Setter private Boolean odExcessAllowed;
|
|
||||||
@Getter @Setter private BigDecimal odExcessLimitPercent;
|
|
||||||
@Getter @Setter private BigDecimal odPenalRate;
|
|
||||||
|
|
||||||
// Complex objects wrapped as Maps/Lists so Jackson converts them into the exact JSON your UI expects
|
// Complex objects wrapped as Maps/Lists so Jackson converts them into the exact JSON your UI expects
|
||||||
@Getter @Setter private Map<String, Object> accountFacilityConfig;
|
@Getter @Setter private Map<String, Object> accountFacilityConfig;
|
||||||
@@ -387,11 +383,6 @@ public final class SavingsProductData implements Serializable {
|
|||||||
target.setAvgBalanceCalcPeriod(source.getAvgBalanceCalcPeriod());
|
target.setAvgBalanceCalcPeriod(source.getAvgBalanceCalcPeriod());
|
||||||
target.setValidForPeriod(source.getValidForPeriod());
|
target.setValidForPeriod(source.getValidForPeriod());
|
||||||
target.setOdAccountClosedZeroBalanceInDayClosing(source.getOdAccountClosedZeroBalanceInDayClosing());
|
target.setOdAccountClosedZeroBalanceInDayClosing(source.getOdAccountClosedZeroBalanceInDayClosing());
|
||||||
target.setOdMinLimit(source.getOdMinLimit());
|
|
||||||
target.setOdSecurityType(source.getOdSecurityType());
|
|
||||||
target.setOdExcessAllowed(source.getOdExcessAllowed());
|
|
||||||
target.setOdExcessLimitPercent(source.getOdExcessLimitPercent());
|
|
||||||
target.setOdPenalRate(source.getOdPenalRate());
|
|
||||||
target.setOtherGlMappingsMap(source.getOtherGlMappingsMap());
|
target.setOtherGlMappingsMap(source.getOtherGlMappingsMap());
|
||||||
target.setAccountFacilityConfig(source.getAccountFacilityConfig());
|
target.setAccountFacilityConfig(source.getAccountFacilityConfig());
|
||||||
target.setAccountFacilityLimits(source.getAccountFacilityLimits());
|
target.setAccountFacilityLimits(source.getAccountFacilityLimits());
|
||||||
|
|||||||
+1
-10
@@ -256,16 +256,7 @@ public final class PostingPeriod {
|
|||||||
this.daysInYear = daysInYear;
|
this.daysInYear = daysInYear;
|
||||||
this.compoundingPeriods = compoundingPeriods;
|
this.compoundingPeriods = compoundingPeriods;
|
||||||
|
|
||||||
if (isUserPosting) {
|
if (isSavingsInterestPostingAtCurrentPeriodEnd) {
|
||||||
// "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,10 +797,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -76,8 +76,7 @@ public class CurrentAccount extends AbstractAuditableCustom {
|
|||||||
@Column(name = "status_enum", nullable = false)
|
@Column(name = "status_enum", nullable = false)
|
||||||
private Integer statusEnum = 100;
|
private Integer statusEnum = 100;
|
||||||
|
|
||||||
// SubStatus, same codes as savings: 0=None, 100=Inactive, 200=Dormant, 300=Escheat, 400=Block, 500=BlockCredit,
|
// SubStatus: 0=None, 100=Inactive, 200=Dormant, 300=Block, 400=BlockDebit, 500=BlockCredit
|
||||||
// 600=BlockDebit
|
|
||||||
@Column(name = "sub_status_enum", nullable = false)
|
@Column(name = "sub_status_enum", nullable = false)
|
||||||
private Integer subStatusEnum = 0;
|
private Integer subStatusEnum = 0;
|
||||||
|
|
||||||
|
|||||||
+4
@@ -77,6 +77,9 @@ 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;
|
||||||
|
|
||||||
@@ -117,6 +120,7 @@ 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());
|
||||||
|
|||||||
+3
@@ -136,6 +136,9 @@ 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;
|
||||||
|
|
||||||
|
|||||||
+1
-7
@@ -38,12 +38,6 @@ public interface CurrentAccountRepository extends JpaRepository<CurrentAccount,
|
|||||||
@Query("SELECT ca FROM CurrentAccount ca WHERE ca.client.id = :clientId AND ca.statusEnum != 600")
|
@Query("SELECT ca FROM CurrentAccount ca WHERE ca.client.id = :clientId AND ca.statusEnum != 600")
|
||||||
List<CurrentAccount> findActiveByClientId(@Param("clientId") Long clientId);
|
List<CurrentAccount> findActiveByClientId(@Param("clientId") Long clientId);
|
||||||
|
|
||||||
// statusEnum != 600 (CLOSED) let a client re-apply for the same product only once every prior
|
@Query("SELECT COUNT(ca) FROM CurrentAccount ca WHERE ca.client.id = :clientId AND ca.productId = :productId AND ca.statusEnum != 600")
|
||||||
// account on it was closed -- but 400 (WITHDRAWN) and 500 (REJECTED), see CurrentAccount.withdraw()
|
|
||||||
// /.reject(), are equally terminal and were still being counted as "still has one", blocking a
|
|
||||||
// fresh application the client should have been free to make. 100/200/300 (submitted, approved,
|
|
||||||
// active) are the only statuses a second application on the same product should actually collide
|
|
||||||
// with.
|
|
||||||
@Query("SELECT COUNT(ca) FROM CurrentAccount ca WHERE ca.client.id = :clientId AND ca.productId = :productId AND ca.statusEnum IN (100, 200, 300)")
|
|
||||||
long countActiveByClientAndProduct(@Param("clientId") Long clientId, @Param("productId") Long productId);
|
long countActiveByClientAndProduct(@Param("clientId") Long clientId, @Param("productId") Long productId);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-15
@@ -46,7 +46,7 @@ public class CurrentAccountTransaction extends AbstractPersistableCustom<Long> {
|
|||||||
@JoinColumn(name = "office_id", nullable = false)
|
@JoinColumn(name = "office_id", nullable = false)
|
||||||
private Office office;
|
private Office office;
|
||||||
|
|
||||||
// 1=Deposit, 2=Withdrawal, 3=InterestPosting, 4=Charge, 5=ChargeWaiver, 6=Transfer, 7=Reversal, 19=Escheat
|
// 1=Deposit, 2=Withdrawal, 3=InterestPosting, 4=Charge, 5=ChargeWaiver, 6=Transfer, 7=Reversal
|
||||||
@Column(name = "transaction_type_enum", nullable = false)
|
@Column(name = "transaction_type_enum", nullable = false)
|
||||||
private Integer transactionTypeEnum;
|
private Integer transactionTypeEnum;
|
||||||
|
|
||||||
@@ -176,20 +176,6 @@ public class CurrentAccountTransaction extends AbstractPersistableCustom<Long> {
|
|||||||
return txn;
|
return txn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// transactionTypeEnum = 19 (Escheat) -- the whole balance moved to the escheat liability, same code as savings
|
|
||||||
public static CurrentAccountTransaction escheat(CurrentAccount account, Office office,
|
|
||||||
BigDecimal amount, LocalDate transactionDate) {
|
|
||||||
CurrentAccountTransaction txn = new CurrentAccountTransaction();
|
|
||||||
txn.currentAccount = account;
|
|
||||||
txn.office = office;
|
|
||||||
txn.transactionTypeEnum = 19;
|
|
||||||
txn.amount = amount;
|
|
||||||
txn.transactionDate = transactionDate;
|
|
||||||
txn.remarks = "Escheat: dormant for the product's escheat period, balance moved to the escheat liability";
|
|
||||||
txn.submittedOnDate = transactionDate;
|
|
||||||
return txn;
|
|
||||||
}
|
|
||||||
|
|
||||||
// transactionTypeEnum = 7 (Reversal / Undo)
|
// transactionTypeEnum = 7 (Reversal / Undo)
|
||||||
public static CurrentAccountTransaction reversal(CurrentAccount account, Office office,
|
public static CurrentAccountTransaction reversal(CurrentAccount account, Office office,
|
||||||
BigDecimal amount, LocalDate transactionDate, Long originalTransactionId) {
|
BigDecimal amount, LocalDate transactionDate, Long originalTransactionId) {
|
||||||
|
|||||||
-6
@@ -36,10 +36,4 @@ public interface CurrentAccountTransactionWritePlatformService {
|
|||||||
CommandProcessingResult reverseTransaction(Long accountId, Long transactionId, boolean isReversal, JsonCommand command);
|
CommandProcessingResult reverseTransaction(Long accountId, Long transactionId, boolean isReversal, JsonCommand command);
|
||||||
|
|
||||||
CommandProcessingResult adjustTransaction(Long accountId, Long transactionId, JsonCommand command);
|
CommandProcessingResult adjustTransaction(Long accountId, Long transactionId, JsonCommand command);
|
||||||
|
|
||||||
/**
|
|
||||||
* Escheats a dormant current account: moves its whole balance to the escheat liability and closes it. Run by the
|
|
||||||
* nightly dormancy job; returns false, leaving the account untouched, where it cannot safely be done.
|
|
||||||
*/
|
|
||||||
boolean escheat(Long accountId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-112
@@ -44,13 +44,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minRequi
|
|||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nameParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nameParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateOverdraftParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateOverdraftParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalAnnualInterestRateParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odExcessAllowedParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odExcessLimitPercentParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odMinLimitParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odPenalRateParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odSecurityTypeParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.validForPeriodParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shortNameParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.taxGroupIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName;
|
||||||
@@ -142,17 +136,7 @@ public class CurrentProductDataValidator {
|
|||||||
SavingsApiConstants.balanceSwapOptionParamName, SavingsApiConstants.msmeParamName,
|
SavingsApiConstants.balanceSwapOptionParamName, SavingsApiConstants.msmeParamName,
|
||||||
SavingsApiConstants.autoDebitFacilityParamName, SavingsApiConstants.avgBalanceCalcPeriodParamName,
|
SavingsApiConstants.autoDebitFacilityParamName, SavingsApiConstants.avgBalanceCalcPeriodParamName,
|
||||||
SavingsApiConstants.validForPeriodParamName, SavingsApiConstants.odAccountClosedZeroBalanceInDayClosingParamName,
|
SavingsApiConstants.validForPeriodParamName, SavingsApiConstants.odAccountClosedZeroBalanceInDayClosingParamName,
|
||||||
odMinLimitParamName, odSecurityTypeParamName, odExcessAllowedParamName, odExcessLimitPercentParamName,
|
SavingsApiConstants.glAccountIdParamName));
|
||||||
odPenalRateParamName, SavingsApiConstants.glAccountIdParamName));
|
|
||||||
|
|
||||||
/** How long an OD sanction on this scheme runs before it has to be renewed. */
|
|
||||||
private static final List<String> OD_VALIDITY_PERIODS = List.of("3M", "6M", "1Y", "2Y", "3Y");
|
|
||||||
/** CLEAN: no security needed; SECURED: backed by FD lien, property etc.; BOTH: decided per sanction. */
|
|
||||||
private static final List<String> OD_SECURITY_TYPES = List.of("CLEAN", "SECURED", "BOTH");
|
|
||||||
/** A sanity ceiling on the OD rate, catching a limit keyed into the rate field. */
|
|
||||||
private static final BigDecimal MAX_OVERDRAFT_RATE = new BigDecimal("50");
|
|
||||||
/** Excess drawing is a small tolerance over the limit, never a second limit. */
|
|
||||||
private static final BigDecimal MAX_EXCESS_PERCENT = new BigDecimal("25");
|
|
||||||
|
|
||||||
public void validateForCreate(final String json) {
|
public void validateForCreate(final String json) {
|
||||||
if (StringUtils.isBlank(json)) {
|
if (StringUtils.isBlank(json)) {
|
||||||
@@ -276,10 +260,10 @@ public class CurrentProductDataValidator {
|
|||||||
if (AccountingValidations.isCashBasedAccounting(accountingRuleType)
|
if (AccountingValidations.isCashBasedAccounting(accountingRuleType)
|
||||||
|| AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) {
|
|| AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) {
|
||||||
savingsProductAccountingDataValidator.evaluateProductAccountingData(accountingRuleType, isDormancyActive, element,
|
savingsProductAccountingDataValidator.evaluateProductAccountingData(accountingRuleType, isDormancyActive, element,
|
||||||
baseDataValidator, DepositAccountType.CURRENT_DEPOSIT, true);
|
baseDataValidator, DepositAccountType.SAVINGS_DEPOSIT, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOverdraftParams(baseDataValidator, element, null);
|
validateOverdraftParams(baseDataValidator, element);
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(minBalanceForInterestCalculationParamName, element)) {
|
if (this.fromApiJsonHelper.parameterExists(minBalanceForInterestCalculationParamName, element)) {
|
||||||
final BigDecimal minBalanceForInterestCalculation = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(minBalanceForInterestCalculationParamName, element);
|
final BigDecimal minBalanceForInterestCalculation = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(minBalanceForInterestCalculationParamName, element);
|
||||||
@@ -374,7 +358,7 @@ public class CurrentProductDataValidator {
|
|||||||
if (AccountingValidations.isCashBasedAccounting(accountingRuleType)
|
if (AccountingValidations.isCashBasedAccounting(accountingRuleType)
|
||||||
|| AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) {
|
|| AccountingValidations.isAccrualPeriodicBasedAccounting(accountingRuleType)) {
|
||||||
savingsProductAccountingDataValidator.evaluateProductAccountingData(accountingRuleType, isDormancyActive, element,
|
savingsProductAccountingDataValidator.evaluateProductAccountingData(accountingRuleType, isDormancyActive, element,
|
||||||
baseDataValidator, DepositAccountType.CURRENT_DEPOSIT, false);
|
baseDataValidator, DepositAccountType.SAVINGS_DEPOSIT, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null != isDormancyActive && isDormancyActive) {
|
if (null != isDormancyActive && isDormancyActive) {
|
||||||
@@ -396,7 +380,7 @@ public class CurrentProductDataValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOverdraftParams(baseDataValidator, element, product);
|
validateOverdraftParams(baseDataValidator, element);
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(minBalanceForInterestCalculationParamName, element)) {
|
if (this.fromApiJsonHelper.parameterExists(minBalanceForInterestCalculationParamName, element)) {
|
||||||
final BigDecimal minBalanceForInterestCalculation = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(minBalanceForInterestCalculationParamName, element);
|
final BigDecimal minBalanceForInterestCalculation = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(minBalanceForInterestCalculationParamName, element);
|
||||||
@@ -414,103 +398,23 @@ public class CurrentProductDataValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void validateOverdraftParams(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||||
* The overdraft section of a current product is the scheme every account's OD sanction is held to, so when OD is
|
|
||||||
* allowed it has to be complete: a ceiling (overdraftLimit, an amount, not a percentage), an optional floor, a rate,
|
|
||||||
* a sanction validity and whether the limit may be clean or must be secured. Interest on an OD is applied monthly,
|
|
||||||
* as RBI requires for advances, or at most quarterly.
|
|
||||||
*
|
|
||||||
* On update, product is the stored product and fills in any value the request leaves out, so a partial edit is
|
|
||||||
* checked against the scheme it will produce; on create it is null.
|
|
||||||
*/
|
|
||||||
private void validateOverdraftParams(final DataValidatorBuilder baseDataValidator, final JsonElement element,
|
|
||||||
final CurrentProduct product) {
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(allowOverdraftParamName, element)) {
|
if (this.fromApiJsonHelper.parameterExists(allowOverdraftParamName, element)) {
|
||||||
final Boolean allowOverdraft = this.fromApiJsonHelper.extractBooleanNamed(allowOverdraftParamName, element);
|
final Boolean allowOverdraft = this.fromApiJsonHelper.extractBooleanNamed(allowOverdraftParamName, element);
|
||||||
baseDataValidator.reset().parameter(allowOverdraftParamName).value(allowOverdraft).ignoreIfNull().validateForBooleanValue();
|
baseDataValidator.reset().parameter(allowOverdraftParamName).value(allowOverdraft).ignoreIfNull().validateForBooleanValue();
|
||||||
}
|
}
|
||||||
final boolean allowOverdraft = this.fromApiJsonHelper.parameterExists(allowOverdraftParamName, element)
|
if (this.fromApiJsonHelper.parameterExists(overdraftLimitParamName, element)) {
|
||||||
? BooleanUtils.isTrue(this.fromApiJsonHelper.extractBooleanNamed(allowOverdraftParamName, element))
|
final BigDecimal overdraftLimit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(overdraftLimitParamName, element);
|
||||||
: product != null && product.isAllowOverdraft();
|
baseDataValidator.reset().parameter(overdraftLimitParamName).value(overdraftLimit).ignoreIfNull().zeroOrPositiveAmount();
|
||||||
|
|
||||||
final BigDecimal maxLimit = bigDecimalOrStored(element, overdraftLimitParamName,
|
|
||||||
product == null ? null : product.getOverdraftLimit());
|
|
||||||
final BigDecimal minLimit = bigDecimalOrStored(element, odMinLimitParamName, product == null ? null : product.getOdMinLimit());
|
|
||||||
final BigDecimal rate = bigDecimalOrStored(element, nominalAnnualInterestRateOverdraftParamName,
|
|
||||||
product == null ? null : product.getNominalAnnualInterestRateOverdraft());
|
|
||||||
final BigDecimal minOdForInterest = bigDecimalOrStored(element, minOverdraftForInterestCalculationParamName,
|
|
||||||
product == null ? null : product.getMinOverdraftForInterestCalculation());
|
|
||||||
|
|
||||||
if (!allowOverdraft) {
|
|
||||||
baseDataValidator.reset().parameter(overdraftLimitParamName).value(maxLimit).ignoreIfNull().zeroOrPositiveAmount();
|
|
||||||
baseDataValidator.reset().parameter(nominalAnnualInterestRateOverdraftParamName).value(rate).ignoreIfNull()
|
|
||||||
.zeroOrPositiveAmount();
|
|
||||||
baseDataValidator.reset().parameter(minOverdraftForInterestCalculationParamName).value(minOdForInterest).ignoreIfNull()
|
|
||||||
.zeroOrPositiveAmount();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(nominalAnnualInterestRateOverdraftParamName, element)) {
|
||||||
baseDataValidator.reset().parameter(overdraftLimitParamName).value(maxLimit).notNull().positiveAmount();
|
final BigDecimal nominalAnnualInterestRateOverdraft = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(nominalAnnualInterestRateOverdraftParamName, element);
|
||||||
baseDataValidator.reset().parameter(odMinLimitParamName).value(minLimit).ignoreIfNull().zeroOrPositiveAmount();
|
baseDataValidator.reset().parameter(nominalAnnualInterestRateOverdraftParamName).value(nominalAnnualInterestRateOverdraft).ignoreIfNull().zeroOrPositiveAmount();
|
||||||
if (minLimit != null && maxLimit != null && minLimit.compareTo(maxLimit) > 0) {
|
|
||||||
baseDataValidator.reset().parameter(odMinLimitParamName).value(minLimit)
|
|
||||||
.failWithCodeAndMessage("cannot.exceed.maximum.overdraft.limit", "Minimum OD limit cannot exceed the maximum OD limit.");
|
|
||||||
}
|
}
|
||||||
baseDataValidator.reset().parameter(nominalAnnualInterestRateOverdraftParamName).value(rate).notNull().positiveAmount()
|
if (this.fromApiJsonHelper.parameterExists(minOverdraftForInterestCalculationParamName, element)) {
|
||||||
.notGreaterThanMax(MAX_OVERDRAFT_RATE);
|
final BigDecimal minOverdraftForInterestCalculation = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(minOverdraftForInterestCalculationParamName, element);
|
||||||
baseDataValidator.reset().parameter(minOverdraftForInterestCalculationParamName).value(minOdForInterest).ignoreIfNull()
|
baseDataValidator.reset().parameter(minOverdraftForInterestCalculationParamName).value(minOverdraftForInterestCalculation).ignoreIfNull().zeroOrPositiveAmount();
|
||||||
.zeroOrPositiveAmount();
|
|
||||||
|
|
||||||
final String validity = stringOrStored(element, validForPeriodParamName, product == null ? null : product.getValidForPeriod());
|
|
||||||
baseDataValidator.reset().parameter(validForPeriodParamName).value(validity).notBlank()
|
|
||||||
.isOneOfTheseStringValues(OD_VALIDITY_PERIODS);
|
|
||||||
|
|
||||||
final String securityType = stringOrStored(element, odSecurityTypeParamName,
|
|
||||||
product == null ? null : product.getOdSecurityType());
|
|
||||||
baseDataValidator.reset().parameter(odSecurityTypeParamName).value(securityType).notBlank()
|
|
||||||
.isOneOfTheseStringValues(OD_SECURITY_TYPES);
|
|
||||||
|
|
||||||
// Excess drawing past the sanctioned limit. Off means a withdrawal beyond the limit is refused. On, the
|
|
||||||
// excess is capped at a percentage of the limit and carries a penal charge on the excess alone -- a separate
|
|
||||||
// charge, as RBI's 2023 penal-charges directions require, not a higher rate on the whole balance.
|
|
||||||
final boolean excessAllowed = this.fromApiJsonHelper.parameterExists(odExcessAllowedParamName, element)
|
|
||||||
? BooleanUtils.isTrue(this.fromApiJsonHelper.extractBooleanNamed(odExcessAllowedParamName, element))
|
|
||||||
: product != null && product.isOdExcessAllowed();
|
|
||||||
if (excessAllowed) {
|
|
||||||
final BigDecimal excessPercent = bigDecimalOrStored(element, odExcessLimitPercentParamName,
|
|
||||||
product == null ? null : product.getOdExcessLimitPercent());
|
|
||||||
baseDataValidator.reset().parameter(odExcessLimitPercentParamName).value(excessPercent).notNull().positiveAmount()
|
|
||||||
.notGreaterThanMax(MAX_EXCESS_PERCENT);
|
|
||||||
final BigDecimal penalRate = bigDecimalOrStored(element, odPenalRateParamName,
|
|
||||||
product == null ? null : product.getOdPenalRate());
|
|
||||||
baseDataValidator.reset().parameter(odPenalRateParamName).value(penalRate).notNull().positiveAmount()
|
|
||||||
.notGreaterThanMax(MAX_OVERDRAFT_RATE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final Integer postingPeriod = this.fromApiJsonHelper.parameterExists(interestPostingPeriodTypeParamName, element)
|
|
||||||
? this.fromApiJsonHelper.extractIntegerSansLocaleNamed(interestPostingPeriodTypeParamName, element)
|
|
||||||
: product == null ? null : product.getInterestPostingPeriodType();
|
|
||||||
baseDataValidator.reset().parameter(interestPostingPeriodTypeParamName).value(postingPeriod).ignoreIfNull()
|
|
||||||
.isOneOfTheseValues(SavingsPostingInterestPeriodType.MONTHLY.getValue(),
|
|
||||||
SavingsPostingInterestPeriodType.QUATERLY.getValue());
|
|
||||||
|
|
||||||
final Integer daysInYear = this.fromApiJsonHelper.parameterExists(interestCalculationDaysInYearTypeParamName, element)
|
|
||||||
? this.fromApiJsonHelper.extractIntegerSansLocaleNamed(interestCalculationDaysInYearTypeParamName, element)
|
|
||||||
: product == null ? null : product.getInterestCalculationDaysInYearType();
|
|
||||||
baseDataValidator.reset().parameter(interestCalculationDaysInYearTypeParamName).value(daysInYear).ignoreIfNull()
|
|
||||||
.isOneOfTheseValues(SavingsInterestCalculationDaysInYearType.DAYS_360.getValue(),
|
|
||||||
SavingsInterestCalculationDaysInYearType.DAYS_365.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal bigDecimalOrStored(final JsonElement element, final String paramName, final BigDecimal stored) {
|
|
||||||
return this.fromApiJsonHelper.parameterExists(paramName, element)
|
|
||||||
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(paramName, element)
|
|
||||||
: stored;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String stringOrStored(final JsonElement element, final String paramName, final String stored) {
|
|
||||||
return this.fromApiJsonHelper.parameterExists(paramName, element) ? this.fromApiJsonHelper.extractStringNamed(paramName, element)
|
|
||||||
: stored;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateLienParams(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
private void validateLienParams(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||||
@@ -526,7 +430,7 @@ public class CurrentProductDataValidator {
|
|||||||
&& BooleanUtils.isTrue(this.fromApiJsonHelper.extractBooleanNamed(allowOverdraftParamName, element))) {
|
&& BooleanUtils.isTrue(this.fromApiJsonHelper.extractBooleanNamed(allowOverdraftParamName, element))) {
|
||||||
final BigDecimal maxAllowedLienLimit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(maxAllowedLienLimitParamName, element);
|
final BigDecimal maxAllowedLienLimit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(maxAllowedLienLimitParamName, element);
|
||||||
final BigDecimal overdraftLimit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(overdraftLimitParamName, element);
|
final BigDecimal overdraftLimit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(overdraftLimitParamName, element);
|
||||||
if (overdraftLimit != null && maxAllowedLienLimit != null && overdraftLimit.compareTo(maxAllowedLienLimit) > 0) {
|
if (overdraftLimit.compareTo(maxAllowedLienLimit) > 0) {
|
||||||
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("Overdraft.limit.can.not.be.greater.than.lien.limit");
|
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("Overdraft.limit.can.not.be.greater.than.lien.limit");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-68
@@ -68,11 +68,6 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebi
|
|||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.avgBalanceCalcPeriodParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.avgBalanceCalcPeriodParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.validForPeriodParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.validForPeriodParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odAccountClosedZeroBalanceInDayClosingParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odAccountClosedZeroBalanceInDayClosingParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odMinLimitParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odSecurityTypeParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odExcessAllowedParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odExcessLimitPercentParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odPenalRateParamName;
|
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import jakarta.persistence.CascadeType;
|
import jakarta.persistence.CascadeType;
|
||||||
@@ -295,25 +290,6 @@ public class CurrentProduct extends AbstractPersistableCustom<Long> {
|
|||||||
@Column(name = "od_account_closed_zero_balance_in_day_closing")
|
@Column(name = "od_account_closed_zero_balance_in_day_closing")
|
||||||
private boolean odAccountClosedZeroBalanceInDayClosing = false;
|
private boolean odAccountClosedZeroBalanceInDayClosing = false;
|
||||||
|
|
||||||
// The smallest limit an account on this scheme may be sanctioned; overdraftLimit is the largest.
|
|
||||||
@Column(name = "od_min_limit", scale = 6, precision = 19)
|
|
||||||
private BigDecimal odMinLimit;
|
|
||||||
|
|
||||||
// CLEAN, SECURED or BOTH: whether a sanction must be backed by security (FD lien, property...).
|
|
||||||
@Column(name = "od_security_type", length = 20)
|
|
||||||
private String odSecurityType;
|
|
||||||
|
|
||||||
// Excess drawing past the sanctioned limit: off means it is blocked. When on, odExcessLimitPercent caps the excess
|
|
||||||
// as a percentage of the limit and odPenalRate (% p.a.) is charged on the excess alone.
|
|
||||||
@Column(name = "od_excess_allowed")
|
|
||||||
private boolean odExcessAllowed = false;
|
|
||||||
|
|
||||||
@Column(name = "od_excess_limit_percent", scale = 6, precision = 19)
|
|
||||||
private BigDecimal odExcessLimitPercent;
|
|
||||||
|
|
||||||
@Column(name = "od_penal_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal odPenalRate;
|
|
||||||
|
|
||||||
@OneToOne(cascade = CascadeType.ALL, mappedBy = "currentProduct", orphanRemoval = true)
|
@OneToOne(cascade = CascadeType.ALL, mappedBy = "currentProduct", orphanRemoval = true)
|
||||||
private CurrentProductFacilityConfig facilityConfig;
|
private CurrentProductFacilityConfig facilityConfig;
|
||||||
|
|
||||||
@@ -828,45 +804,11 @@ public class CurrentProduct extends AbstractPersistableCustom<Long> {
|
|||||||
actualChanges.put(odAccountClosedZeroBalanceInDayClosingParamName, newValue);
|
actualChanges.put(odAccountClosedZeroBalanceInDayClosingParamName, newValue);
|
||||||
this.odAccountClosedZeroBalanceInDayClosing = newValue;
|
this.odAccountClosedZeroBalanceInDayClosing = newValue;
|
||||||
}
|
}
|
||||||
if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(odMinLimitParamName, this.odMinLimit)) {
|
|
||||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(odMinLimitParamName);
|
|
||||||
actualChanges.put(odMinLimitParamName, newValue);
|
|
||||||
this.odMinLimit = newValue;
|
|
||||||
}
|
|
||||||
if (command.isChangeInStringParameterNamed(odSecurityTypeParamName, this.odSecurityType)) {
|
|
||||||
final String newValue = command.stringValueOfParameterNamed(odSecurityTypeParamName);
|
|
||||||
actualChanges.put(odSecurityTypeParamName, newValue);
|
|
||||||
this.odSecurityType = newValue;
|
|
||||||
}
|
|
||||||
if (command.isChangeInBooleanParameterNamed(odExcessAllowedParamName, this.odExcessAllowed)) {
|
|
||||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(odExcessAllowedParamName);
|
|
||||||
actualChanges.put(odExcessAllowedParamName, newValue);
|
|
||||||
this.odExcessAllowed = newValue;
|
|
||||||
}
|
|
||||||
if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(odExcessLimitPercentParamName, this.odExcessLimitPercent)) {
|
|
||||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(odExcessLimitPercentParamName);
|
|
||||||
actualChanges.put(odExcessLimitPercentParamName, newValue);
|
|
||||||
this.odExcessLimitPercent = newValue;
|
|
||||||
}
|
|
||||||
if (command.isChangeInBigDecimalParameterNamedDefaultingZeroToNull(odPenalRateParamName, this.odPenalRate)) {
|
|
||||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero(odPenalRateParamName);
|
|
||||||
actualChanges.put(odPenalRateParamName, newValue);
|
|
||||||
this.odPenalRate = newValue;
|
|
||||||
}
|
|
||||||
if (!this.odExcessAllowed) {
|
|
||||||
this.odExcessLimitPercent = null;
|
|
||||||
this.odPenalRate = null;
|
|
||||||
}
|
|
||||||
// Clear OD-specific fields when overdraft is disabled
|
// Clear OD-specific fields when overdraft is disabled
|
||||||
if (!this.allowOverdraft) {
|
if (!this.allowOverdraft) {
|
||||||
this.avgBalanceCalcPeriod = null;
|
this.avgBalanceCalcPeriod = null;
|
||||||
this.validForPeriod = null;
|
this.validForPeriod = null;
|
||||||
this.odAccountClosedZeroBalanceInDayClosing = false;
|
this.odAccountClosedZeroBalanceInDayClosing = false;
|
||||||
this.odMinLimit = null;
|
|
||||||
this.odSecurityType = null;
|
|
||||||
this.odExcessAllowed = false;
|
|
||||||
this.odExcessLimitPercent = null;
|
|
||||||
this.odPenalRate = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return actualChanges;
|
return actualChanges;
|
||||||
@@ -948,14 +890,4 @@ public class CurrentProduct extends AbstractPersistableCustom<Long> {
|
|||||||
public void setValidForPeriod(String validForPeriod) { this.validForPeriod = validForPeriod; }
|
public void setValidForPeriod(String validForPeriod) { this.validForPeriod = validForPeriod; }
|
||||||
public boolean isOdAccountClosedZeroBalanceInDayClosing() { return odAccountClosedZeroBalanceInDayClosing; }
|
public boolean isOdAccountClosedZeroBalanceInDayClosing() { return odAccountClosedZeroBalanceInDayClosing; }
|
||||||
public void setOdAccountClosedZeroBalanceInDayClosing(boolean val) { this.odAccountClosedZeroBalanceInDayClosing = val; }
|
public void setOdAccountClosedZeroBalanceInDayClosing(boolean val) { this.odAccountClosedZeroBalanceInDayClosing = val; }
|
||||||
public BigDecimal getOdMinLimit() { return odMinLimit; }
|
|
||||||
public void setOdMinLimit(BigDecimal odMinLimit) { this.odMinLimit = odMinLimit; }
|
|
||||||
public String getOdSecurityType() { return odSecurityType; }
|
|
||||||
public void setOdSecurityType(String odSecurityType) { this.odSecurityType = odSecurityType; }
|
|
||||||
public boolean isOdExcessAllowed() { return odExcessAllowed; }
|
|
||||||
public void setOdExcessAllowed(boolean odExcessAllowed) { this.odExcessAllowed = odExcessAllowed; }
|
|
||||||
public BigDecimal getOdExcessLimitPercent() { return odExcessLimitPercent; }
|
|
||||||
public void setOdExcessLimitPercent(BigDecimal odExcessLimitPercent) { this.odExcessLimitPercent = odExcessLimitPercent; }
|
|
||||||
public BigDecimal getOdPenalRate() { return odPenalRate; }
|
|
||||||
public void setOdPenalRate(BigDecimal odPenalRate) { this.odPenalRate = odPenalRate; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-22
@@ -43,11 +43,6 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebi
|
|||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.avgBalanceCalcPeriodParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.avgBalanceCalcPeriodParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.validForPeriodParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.validForPeriodParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odAccountClosedZeroBalanceInDayClosingParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odAccountClosedZeroBalanceInDayClosingParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odMinLimitParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odSecurityTypeParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odExcessAllowedParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odExcessLimitPercentParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.odPenalRateParamName;
|
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.endDateParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.endDateParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.frequencyFixDayParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.frequencyFixDayParamName;
|
||||||
@@ -319,23 +314,6 @@ public class CurrentProductAssembler {
|
|||||||
if (command.parameterExists(odAccountClosedZeroBalanceInDayClosingParamName)) {
|
if (command.parameterExists(odAccountClosedZeroBalanceInDayClosingParamName)) {
|
||||||
product.setOdAccountClosedZeroBalanceInDayClosing(command.booleanPrimitiveValueOfParameterNamed(odAccountClosedZeroBalanceInDayClosingParamName));
|
product.setOdAccountClosedZeroBalanceInDayClosing(command.booleanPrimitiveValueOfParameterNamed(odAccountClosedZeroBalanceInDayClosingParamName));
|
||||||
}
|
}
|
||||||
if (command.parameterExists(odMinLimitParamName)) {
|
|
||||||
product.setOdMinLimit(command.bigDecimalValueOfParameterNamed(odMinLimitParamName));
|
|
||||||
}
|
|
||||||
if (command.parameterExists(odSecurityTypeParamName)) {
|
|
||||||
product.setOdSecurityType(command.stringValueOfParameterNamed(odSecurityTypeParamName));
|
|
||||||
}
|
|
||||||
final boolean excessAllowed = command.parameterExists(odExcessAllowedParamName)
|
|
||||||
&& command.booleanPrimitiveValueOfParameterNamed(odExcessAllowedParamName);
|
|
||||||
product.setOdExcessAllowed(excessAllowed);
|
|
||||||
if (excessAllowed) {
|
|
||||||
if (command.parameterExists(odExcessLimitPercentParamName)) {
|
|
||||||
product.setOdExcessLimitPercent(command.bigDecimalValueOfParameterNamed(odExcessLimitPercentParamName));
|
|
||||||
}
|
|
||||||
if (command.parameterExists(odPenalRateParamName)) {
|
|
||||||
product.setOdPenalRate(command.bigDecimalValueOfParameterNamed(odPenalRateParamName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return product;
|
return product;
|
||||||
@@ -400,6 +378,7 @@ 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;
|
||||||
|
|||||||
+6
@@ -75,6 +75,9 @@ 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;
|
||||||
|
|
||||||
@@ -122,6 +125,8 @@ 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; }
|
||||||
@@ -142,6 +147,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
-115
@@ -1,115 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.currentproduct.service;
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingParams;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a current product's GL mappings to acc_product_mapping under {@link PortfolioProductType#CURRENT}.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* They used to be written by the savings mapping writer under SAVING, keyed only by product id, so a current product
|
|
||||||
* and a savings product with the same id shared rows: two Current Control rows came back for one lookup and the
|
|
||||||
* account's GL posting failed outright, leaving the balance moved with no journal entry. Written under their own type,
|
|
||||||
* the two can never meet.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* A parameter present in the request replaces that one mapping (null clears it); an absent one is left alone, so a
|
|
||||||
* partial edit changes only what it names. Accounting rule NONE clears every mapping.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class CurrentProductGlMappingWriter {
|
|
||||||
|
|
||||||
/** Request parameter -> financial account type (CashAccountsForSavings / AccrualAccountsForSavings). */
|
|
||||||
private static final Map<String, Integer> PARAM_TO_FINANCIAL_TYPE = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.SAVINGS_REFERENCE.getValue(), 1);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.SAVINGS_CONTROL.getValue(), 2);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), 3);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), 4);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), 5);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), 10);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), 11);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.INCOME_FROM_INTEREST.getValue(), 12);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), 13);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), 14);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.FEES_RECEIVABLE.getValue(), 15);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), 16);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.INTEREST_PAYABLE.getValue(), 17);
|
|
||||||
PARAM_TO_FINANCIAL_TYPE.put(SavingProductAccountingParams.INTEREST_RECEIVABLE.getValue(), 18);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int CURRENT = PortfolioProductType.CURRENT.getValue();
|
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the request's mappings to the product and returns what changed, keyed by parameter name.
|
|
||||||
*
|
|
||||||
* @param accountingRule
|
|
||||||
* the product's accounting rule after this request (1 = NONE)
|
|
||||||
*/
|
|
||||||
public Map<String, Object> write(final Long productId, final JsonCommand command, final Integer accountingRule) {
|
|
||||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
|
||||||
if (accountingRule == null || accountingRule == 1) {
|
|
||||||
jdbcTemplate.update("DELETE FROM acc_product_mapping WHERE product_type = ? AND product_id = ?", CURRENT, productId);
|
|
||||||
return changes;
|
|
||||||
}
|
|
||||||
final JsonElement parsed = command.parsedJson();
|
|
||||||
final JsonObject json = parsed != null && parsed.isJsonObject() ? parsed.getAsJsonObject() : new JsonObject();
|
|
||||||
for (final Map.Entry<String, Integer> entry : PARAM_TO_FINANCIAL_TYPE.entrySet()) {
|
|
||||||
final String param = entry.getKey();
|
|
||||||
if (!json.has(param)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final Long glAccountId = json.get(param).isJsonNull() ? null : json.get(param).getAsLong();
|
|
||||||
final Long existing = jdbcTemplate.queryForList(
|
|
||||||
"SELECT gl_account_id FROM acc_product_mapping WHERE product_type = ? AND product_id = ? AND financial_account_type = ?"
|
|
||||||
+ " AND payment_type IS NULL AND charge_id IS NULL",
|
|
||||||
Long.class, CURRENT, productId, entry.getValue()).stream().findFirst().orElse(null);
|
|
||||||
if (java.util.Objects.equals(existing, glAccountId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
jdbcTemplate.update("DELETE FROM acc_product_mapping WHERE product_type = ? AND product_id = ? AND financial_account_type = ?"
|
|
||||||
+ " AND payment_type IS NULL AND charge_id IS NULL", CURRENT, productId, entry.getValue());
|
|
||||||
if (glAccountId != null) {
|
|
||||||
jdbcTemplate.update("INSERT INTO acc_product_mapping (gl_account_id, product_id, product_type, financial_account_type)"
|
|
||||||
+ " VALUES (?, ?, ?, ?)", glAccountId, productId, CURRENT, entry.getValue());
|
|
||||||
}
|
|
||||||
changes.put(param, glAccountId);
|
|
||||||
}
|
|
||||||
return changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Removes every mapping of a deleted product. */
|
|
||||||
public void deleteAll(final Long productId) {
|
|
||||||
jdbcTemplate.update("DELETE FROM acc_product_mapping WHERE product_type = ? AND product_id = ?", CURRENT, productId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-8
@@ -117,6 +117,7 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
|
|||||||
facilityConfigMap.put("neftFacility", facilityConfig.getNeftFacility());
|
facilityConfigMap.put("neftFacility", facilityConfig.getNeftFacility());
|
||||||
facilityConfigMap.put("rtgsFacility", facilityConfig.getRtgsFacility());
|
facilityConfigMap.put("rtgsFacility", facilityConfig.getRtgsFacility());
|
||||||
facilityConfigMap.put("interBankTotalCapFacility", facilityConfig.getInterBankTotalCapFacility());
|
facilityConfigMap.put("interBankTotalCapFacility", facilityConfig.getInterBankTotalCapFacility());
|
||||||
|
facilityConfigMap.put("withdrawalFacility", facilityConfig.getWithdrawalFacility());
|
||||||
facilityConfigMap.put("depositFacilityKyc", facilityConfig.getDepositFacilityKyc());
|
facilityConfigMap.put("depositFacilityKyc", facilityConfig.getDepositFacilityKyc());
|
||||||
facilityConfigMap.put("virtualDebitCard", facilityConfig.getVirtualDebitCard());
|
facilityConfigMap.put("virtualDebitCard", facilityConfig.getVirtualDebitCard());
|
||||||
product.setAccountFacilityConfig(facilityConfigMap);
|
product.setAccountFacilityConfig(facilityConfigMap);
|
||||||
@@ -268,9 +269,6 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
|
|||||||
sqlBuilder.append("cp.balance_swap_option as balanceSwapOption, cp.msme as msme, cp.auto_debit_facility as autoDebitFacility, ");
|
sqlBuilder.append("cp.balance_swap_option as balanceSwapOption, cp.msme as msme, cp.auto_debit_facility as autoDebitFacility, ");
|
||||||
sqlBuilder.append("cp.avg_balance_calc_period as avgBalanceCalcPeriod, cp.valid_for_period as validForPeriod, ");
|
sqlBuilder.append("cp.avg_balance_calc_period as avgBalanceCalcPeriod, cp.valid_for_period as validForPeriod, ");
|
||||||
sqlBuilder.append("cp.od_account_closed_zero_balance_in_day_closing as odAccountClosedZeroBalanceInDayClosing, ");
|
sqlBuilder.append("cp.od_account_closed_zero_balance_in_day_closing as odAccountClosedZeroBalanceInDayClosing, ");
|
||||||
sqlBuilder.append("cp.od_min_limit as odMinLimit, cp.od_security_type as odSecurityType, ");
|
|
||||||
sqlBuilder.append("cp.od_excess_allowed as odExcessAllowed, cp.od_excess_limit_percent as odExcessLimitPercent, ");
|
|
||||||
sqlBuilder.append("cp.od_penal_rate as odPenalRate, ");
|
|
||||||
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = cp.id "
|
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = cp.id "
|
||||||
+ "and spt.product_type = 'CURRENT' and spt.status = 'PENDING') as isPendingApproval ");
|
+ "and spt.product_type = 'CURRENT' and spt.status = 'PENDING') as isPendingApproval ");
|
||||||
|
|
||||||
@@ -396,11 +394,6 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
|
|||||||
productData.setAvgBalanceCalcPeriod(rs.getString("avgBalanceCalcPeriod"));
|
productData.setAvgBalanceCalcPeriod(rs.getString("avgBalanceCalcPeriod"));
|
||||||
productData.setValidForPeriod(rs.getString("validForPeriod"));
|
productData.setValidForPeriod(rs.getString("validForPeriod"));
|
||||||
productData.setOdAccountClosedZeroBalanceInDayClosing(rs.getBoolean("odAccountClosedZeroBalanceInDayClosing"));
|
productData.setOdAccountClosedZeroBalanceInDayClosing(rs.getBoolean("odAccountClosedZeroBalanceInDayClosing"));
|
||||||
productData.setOdMinLimit(rs.getBigDecimal("odMinLimit"));
|
|
||||||
productData.setOdSecurityType(rs.getString("odSecurityType"));
|
|
||||||
productData.setOdExcessAllowed(rs.getBoolean("odExcessAllowed"));
|
|
||||||
productData.setOdExcessLimitPercent(rs.getBigDecimal("odExcessLimitPercent"));
|
|
||||||
productData.setOdPenalRate(rs.getBigDecimal("odPenalRate"));
|
|
||||||
productData.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
|
productData.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
|
||||||
|
|
||||||
return productData;
|
return productData;
|
||||||
|
|||||||
+6
-6
@@ -98,7 +98,6 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
|
|||||||
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
|
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
private final CommandSourceRepository commandSourceRepository;
|
private final CommandSourceRepository commandSourceRepository;
|
||||||
private final CurrentProductGlMappingWriter glMappingWriter;
|
|
||||||
|
|
||||||
private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dae) {
|
private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dae) {
|
||||||
String msgCode = "error.msg." + DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME;
|
String msgCode = "error.msg." + DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME;
|
||||||
@@ -162,8 +161,8 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
|
|||||||
this.depositLimitKycRepository.saveAndFlush(depositLimitKyc);
|
this.depositLimitKycRepository.saveAndFlush(depositLimitKyc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Under the product's own CURRENT type, never SAVING: see CurrentProductGlMappingWriter.
|
this.accountMappingWritePlatformService.createSavingProductToGLAccountMapping(product.getId(), command,
|
||||||
this.glMappingWriter.write(product.getId(), command, product.getAccountingType());
|
DepositAccountType.SAVINGS_DEPOSIT);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
.withEntityId(product.getId()) //
|
.withEntityId(product.getId()) //
|
||||||
@@ -259,8 +258,10 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// accounting related changes
|
// accounting related changes
|
||||||
final Map<String, Object> accountingMappingChanges = this.glMappingWriter.write(product.getId(), command,
|
final boolean accountingTypeChanged = changes.containsKey(accountingRuleParamName);
|
||||||
product.getAccountingType());
|
final Map<String, Object> accountingMappingChanges = this.accountMappingWritePlatformService
|
||||||
|
.updateSavingsProductToGLAccountMapping(product.getId(), command, accountingTypeChanged, product.getAccountingType(),
|
||||||
|
DepositAccountType.SAVINGS_DEPOSIT);
|
||||||
changes.putAll(accountingMappingChanges);
|
changes.putAll(accountingMappingChanges);
|
||||||
|
|
||||||
if (!changes.isEmpty()) {
|
if (!changes.isEmpty()) {
|
||||||
@@ -503,7 +504,6 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
|
|||||||
.orElseThrow(() -> new SavingsProductNotFoundException(productId));
|
.orElseThrow(() -> new SavingsProductNotFoundException(productId));
|
||||||
|
|
||||||
this.currentProductRepository.delete(product);
|
this.currentProductRepository.delete(product);
|
||||||
this.glMappingWriter.deleteAll(productId);
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
.withEntityId(product.getId()) //
|
.withEntityId(product.getId()) //
|
||||||
|
|||||||
+2
-3
@@ -33,7 +33,6 @@ import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDeposit
|
|||||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository;
|
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository;
|
||||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository;
|
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository;
|
||||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
|
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
|
||||||
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductGlMappingWriter;
|
|
||||||
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductReadPlatformService;
|
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductWritePlatformService;
|
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductWritePlatformService;
|
||||||
@@ -79,11 +78,11 @@ public class CurrentProductConfiguration {
|
|||||||
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository,
|
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository,
|
||||||
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
|
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
|
||||||
FromJsonHelper fromApiJsonHelper,
|
FromJsonHelper fromApiJsonHelper,
|
||||||
CommandSourceRepository commandSourceRepository, CurrentProductGlMappingWriter glMappingWriter) {
|
CommandSourceRepository commandSourceRepository) {
|
||||||
return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator,
|
return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator,
|
||||||
currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository,
|
currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository,
|
||||||
currentFacilityLimitRepository, currentDepositLimitKycRepository, productPendingEditRepository,
|
currentFacilityLimitRepository, currentDepositLimitKycRepository, productPendingEditRepository,
|
||||||
pendingEditSubmittedFieldRepository, pendingEditChargeRepository, pendingEditFacilityLimitRepository,
|
pendingEditSubmittedFieldRepository, pendingEditChargeRepository, pendingEditFacilityLimitRepository,
|
||||||
pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository, glMappingWriter);
|
pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-243
@@ -1,243 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.accounting.balancesheet.api;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import jakarta.ws.rs.Consumes;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.QueryParam;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An "as on date" balance sheet built directly off the live chart of accounts and journal entries,
|
|
||||||
* rather than a stock Fineract trial balance report — this reproduces the specific sub-group-wise
|
|
||||||
* layout this bank's own paper balance sheet uses: every top-level Header account (Capital, Deposit
|
|
||||||
* Account, Other Liabilities, ...) is one named block, with its Detail (and any nested Header)
|
|
||||||
* children indented underneath it and a subtotal on the block itself. A Detail account with no
|
|
||||||
* Header at all (see GLAccountTransactionGuard's sibling concern - some accounts are seeded standalone)
|
|
||||||
* still needs to appear somewhere, so it is listed as its own root-level line rather than silently
|
|
||||||
* dropped.
|
|
||||||
*
|
|
||||||
* A Header account never itself carries a journal entry (see the header/detail discussion this
|
|
||||||
* feature grew out of), so its balance is always the recursive sum of its children - never read off
|
|
||||||
* acc_gl_journal_entry directly.
|
|
||||||
*/
|
|
||||||
@Path("/v1/accounting/balancesheet")
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
public class BalanceSheetApiResource {
|
|
||||||
|
|
||||||
private static final String RESOURCE_NAME_FOR_PERMISSION = "GLACCOUNT";
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final Gson gson = new Gson();
|
|
||||||
|
|
||||||
// classification_enum values (see acc_gl_account / GLAccountType).
|
|
||||||
private static final int ASSET = 1;
|
|
||||||
private static final int LIABILITY = 2;
|
|
||||||
private static final int EQUITY = 3;
|
|
||||||
private static final int INCOME = 4;
|
|
||||||
private static final int EXPENSE = 5;
|
|
||||||
// account_usage values (see GLAccountUsage).
|
|
||||||
private static final int DETAIL = 1;
|
|
||||||
private static final int HEADER = 2;
|
|
||||||
|
|
||||||
private static final class Node {
|
|
||||||
|
|
||||||
Long id;
|
|
||||||
String name;
|
|
||||||
String glCode;
|
|
||||||
int usage;
|
|
||||||
int classification;
|
|
||||||
Long parentId;
|
|
||||||
BigDecimal ownBalance = BigDecimal.ZERO;
|
|
||||||
final List<Node> children = new ArrayList<>();
|
|
||||||
|
|
||||||
BigDecimal balance() {
|
|
||||||
if (usage == DETAIL) {
|
|
||||||
return ownBalance;
|
|
||||||
}
|
|
||||||
BigDecimal sum = BigDecimal.ZERO;
|
|
||||||
for (final Node child : children) {
|
|
||||||
sum = sum.add(child.balance());
|
|
||||||
}
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> toJson() {
|
|
||||||
final Map<String, Object> json = new LinkedHashMap<>();
|
|
||||||
json.put("id", id);
|
|
||||||
json.put("name", name);
|
|
||||||
json.put("glCode", glCode);
|
|
||||||
json.put("usage", usage == HEADER ? "HEADER" : "DETAIL");
|
|
||||||
json.put("balance", balance());
|
|
||||||
final List<Map<String, Object>> childJson = new ArrayList<>();
|
|
||||||
for (final Node child : children) {
|
|
||||||
childJson.add(child.toJson());
|
|
||||||
}
|
|
||||||
json.put("children", childJson);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
public String retrieveBalanceSheet(@QueryParam("asOnDate") final String asOnDateParam,
|
|
||||||
@QueryParam("officeId") final Long officeId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
|
|
||||||
|
|
||||||
final LocalDate asOnDate = (asOnDateParam == null || asOnDateParam.isBlank()) ? DateUtils.getBusinessLocalDate()
|
|
||||||
: LocalDate.parse(asOnDateParam);
|
|
||||||
|
|
||||||
final Map<Long, Node> nodesById = new LinkedHashMap<>();
|
|
||||||
for (final Map<String, Object> row : this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT id, name, gl_code, parent_id, account_usage, classification_enum FROM acc_gl_account "
|
|
||||||
+ "WHERE disabled = false ORDER BY gl_code")) {
|
|
||||||
final Node node = new Node();
|
|
||||||
node.id = ((Number) row.get("id")).longValue();
|
|
||||||
node.name = (String) row.get("name");
|
|
||||||
node.glCode = (String) row.get("gl_code");
|
|
||||||
node.parentId = row.get("parent_id") == null ? null : ((Number) row.get("parent_id")).longValue();
|
|
||||||
node.usage = ((Number) row.get("account_usage")).intValue();
|
|
||||||
node.classification = ((Number) row.get("classification_enum")).intValue();
|
|
||||||
nodesById.put(node.id, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// entry_date (not transaction_date) and "reversed = false" are this codebase's settled
|
|
||||||
// convention for reading balances off the ledger - ProfitAndLossApiResource, the dividend
|
|
||||||
// reports and IncomeProfitTransferService all cut the same way, so this report agrees with
|
|
||||||
// them rather than quietly producing a second set of numbers.
|
|
||||||
//
|
|
||||||
// The reversal_id exclusion covers a case that plain "reversed = false" gets wrong. This
|
|
||||||
// codebase reverses in two different ways: the clerk/sponsor-DD undo paths just flip the
|
|
||||||
// flag on the original leg (no counter entry - excluding it is exactly right), while
|
|
||||||
// Fineract's own revertJournalEntry additionally posts an opposite-sign counter entry and
|
|
||||||
// links it back through reversal_id. For that second kind, dropping only the original would
|
|
||||||
// leave its counter entry standing alone and swing the balance by the full amount in the
|
|
||||||
// wrong direction, so the counter entry has to go too.
|
|
||||||
final Map<Long, BigDecimal[]> balances = new LinkedHashMap<>();
|
|
||||||
final String balanceSql = "SELECT account_id, "
|
|
||||||
+ "COALESCE(SUM(CASE WHEN type_enum = 2 THEN amount ELSE 0 END), 0) AS total_debit, "
|
|
||||||
+ "COALESCE(SUM(CASE WHEN type_enum = 1 THEN amount ELSE 0 END), 0) AS total_credit "
|
|
||||||
+ "FROM acc_gl_journal_entry WHERE entry_date <= ? "
|
|
||||||
+ "AND (reversed IS NULL OR reversed = false) "
|
|
||||||
+ "AND id NOT IN (SELECT reversal_id FROM acc_gl_journal_entry WHERE reversal_id IS NOT NULL)"
|
|
||||||
+ (officeId != null ? " AND office_id = ?" : "") + " GROUP BY account_id";
|
|
||||||
final List<Map<String, Object>> balanceRows = officeId != null
|
|
||||||
? this.jdbcTemplate.queryForList(balanceSql, java.sql.Date.valueOf(asOnDate), officeId)
|
|
||||||
: this.jdbcTemplate.queryForList(balanceSql, java.sql.Date.valueOf(asOnDate));
|
|
||||||
for (final Map<String, Object> row : balanceRows) {
|
|
||||||
balances.put(((Number) row.get("account_id")).longValue(),
|
|
||||||
new BigDecimal[] { (BigDecimal) row.get("total_debit"), (BigDecimal) row.get("total_credit") });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Node node : nodesById.values()) {
|
|
||||||
if (node.usage != DETAIL) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final BigDecimal[] debitCredit = balances.get(node.id);
|
|
||||||
if (debitCredit == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final boolean debitNatured = node.classification == ASSET || node.classification == EXPENSE;
|
|
||||||
node.ownBalance = debitNatured ? debitCredit[0].subtract(debitCredit[1]) : debitCredit[1].subtract(debitCredit[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Node> roots = new ArrayList<>();
|
|
||||||
for (final Node node : nodesById.values()) {
|
|
||||||
if (node.parentId == null) {
|
|
||||||
roots.add(node);
|
|
||||||
} else {
|
|
||||||
final Node parent = nodesById.get(node.parentId);
|
|
||||||
if (parent != null) {
|
|
||||||
parent.children.add(node);
|
|
||||||
} else {
|
|
||||||
// Parent was disabled (filtered out above) - surface this node at the top level
|
|
||||||
// rather than silently losing its balance from the report.
|
|
||||||
roots.add(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, Object> debitSide = buildSide("ASSETS", roots, ASSET, EXPENSE);
|
|
||||||
final Map<String, Object> creditSide = buildSide("LIABILITIES", roots, LIABILITY, EQUITY, INCOME);
|
|
||||||
|
|
||||||
final Map<String, Object> response = new LinkedHashMap<>();
|
|
||||||
response.put("asOnDate", asOnDate.toString());
|
|
||||||
response.put("officeId", officeId);
|
|
||||||
response.put("debitSide", debitSide);
|
|
||||||
response.put("creditSide", creditSide);
|
|
||||||
return this.gson.toJson(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> buildSide(final String label, final List<Node> roots, final int... classifications) {
|
|
||||||
final List<Map<String, Object>> groups = new ArrayList<>();
|
|
||||||
BigDecimal sideTotal = BigDecimal.ZERO;
|
|
||||||
for (final int classification : classifications) {
|
|
||||||
final List<Map<String, Object>> nodeJsons = new ArrayList<>();
|
|
||||||
BigDecimal groupTotal = BigDecimal.ZERO;
|
|
||||||
for (final Node root : roots) {
|
|
||||||
if (root.classification != classification) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
nodeJsons.add(root.toJson());
|
|
||||||
groupTotal = groupTotal.add(root.balance());
|
|
||||||
}
|
|
||||||
if (!nodeJsons.isEmpty()) {
|
|
||||||
final Map<String, Object> group = new LinkedHashMap<>();
|
|
||||||
group.put("classification", classificationLabel(classification));
|
|
||||||
group.put("total", groupTotal);
|
|
||||||
group.put("nodes", nodeJsons);
|
|
||||||
groups.add(group);
|
|
||||||
sideTotal = sideTotal.add(groupTotal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final Map<String, Object> side = new LinkedHashMap<>();
|
|
||||||
side.put("label", label);
|
|
||||||
side.put("total", sideTotal);
|
|
||||||
side.put("groups", groups);
|
|
||||||
return side;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String classificationLabel(final int classification) {
|
|
||||||
return switch (classification) {
|
|
||||||
case ASSET -> "ASSET";
|
|
||||||
case LIABILITY -> "LIABILITY";
|
|
||||||
case EQUITY -> "EQUITY";
|
|
||||||
case INCOME -> "INCOME";
|
|
||||||
case EXPENSE -> "EXPENSE";
|
|
||||||
default -> "OTHER";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-112
@@ -1,112 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.accounting.common;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the GL accounts the dividend lifecycle posts to, from the Financial Activity Mappings an administrator
|
|
||||||
* configures.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* These accounts used to be found by hardcoded GL code - {@code BANKPROFITND01} and {@code BANKPROFIT01} - spread
|
|
||||||
* across five classes. A society whose chart of accounts did not happen to use those exact codes got no dividend
|
|
||||||
* journal entries at all, and nothing said so: every call site checked for null and returned quietly. Reading them from
|
|
||||||
* the mapping table instead lets each society name its own accounts, and keeps the whole lifecycle answerable from one
|
|
||||||
* screen.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Resolution is mapping-only, with no fallback to the old codes: a society that has not mapped an account has not made
|
|
||||||
* the decision yet, and quietly posting to an account it never chose is worse than refusing. Every method here throws
|
|
||||||
* rather than returning null, so a missing mapping surfaces at the point of posting with the name of the activity to
|
|
||||||
* configure, instead of leaving a declaration that appears to have worked but moved nothing.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DividendGlAccountResolver {
|
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
|
|
||||||
/** The year's closed-out profit a dividend is declared against. */
|
|
||||||
public Long distributableProfitAccount() {
|
|
||||||
return requireMapped(FinancialActivity.DISTRIBUTABLE_PROFIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Where profit settles once it is no longer distributable: the undistributed remainder, rounding, forfeitures. */
|
|
||||||
public Long retainedProfitAccount() {
|
|
||||||
return requireMapped(FinancialActivity.RETAINED_PROFIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Equity the declared profit is set aside in, between declaration and board approval. */
|
|
||||||
public Long reserveForDividendAccount() {
|
|
||||||
return requireMapped(FinancialActivity.RESERVE_FOR_DIVIDEND);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The liability to members, created on approval and discharged as each member claims. */
|
|
||||||
public Long payableDividendsAccount() {
|
|
||||||
return requireMapped(FinancialActivity.PAYABLE_DIVIDENDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Where the paise a dividend cannot divide evenly are swept.
|
|
||||||
*
|
|
||||||
* Falls back to retained profit when unmapped, which is exactly where the residue went before this account existed
|
|
||||||
* - so a society that does not care to separate it keeps the behaviour it already had, and one that maps it gets
|
|
||||||
* the arithmetic artefact out of a figure it reads as a business result. This is a presentation choice rather than
|
|
||||||
* a decision the society has failed to make, which is why it resolves rather than throwing.
|
|
||||||
*/
|
|
||||||
public Long dividendRoundOffAccount() {
|
|
||||||
final Long roundOff = mappedAccount(FinancialActivity.DIVIDEND_ROUND_OFF);
|
|
||||||
return roundOff != null ? roundOff : retainedProfitAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contra the claim is routed through, so claimed and unclaimed read as two balances rather than one net figure.
|
|
||||||
*
|
|
||||||
* Optional by design - a society that has not mapped it still gets a correct payout, posted straight from Dividend
|
|
||||||
* Payable, just without that separate measure. This is the one account here that may legitimately be absent, so it
|
|
||||||
* returns null instead of throwing.
|
|
||||||
*/
|
|
||||||
public Long dividendPaidAccountOrNull() {
|
|
||||||
return mappedAccount(FinancialActivity.DIVIDEND_PAID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The GL mapped to an activity, or null when the society has not configured it. */
|
|
||||||
public Long mappedAccount(final FinancialActivity activity) {
|
|
||||||
return this.jdbcTemplate
|
|
||||||
.queryForList("SELECT gl_account_id FROM acc_gl_financial_activity_account WHERE financial_activity_type = ?", Long.class,
|
|
||||||
activity.getValue())
|
|
||||||
.stream().findFirst().orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long requireMapped(final FinancialActivity activity) {
|
|
||||||
final Long glAccountId = mappedAccount(activity);
|
|
||||||
if (glAccountId == null) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.financial.activity.mapping." + activity.getCode() + ".not.configured",
|
|
||||||
"No GL account is mapped to the '" + activity + "' financial activity. Map one under Accounting > Financial Activity "
|
|
||||||
+ "Mappings before running the dividend cycle.",
|
|
||||||
activity.getValue());
|
|
||||||
}
|
|
||||||
return glAccountId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-185
@@ -1,185 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.accounting.dividendpaidreport.api;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every declared dividend, financial year by financial year: how much was declared, how much members have collected,
|
|
||||||
* how much is still unclaimed, and how much has come back to profit.
|
|
||||||
*
|
|
||||||
* The GL accounts are shared across years, so their balances only ever give running totals — "how much of FY 2026-27 is
|
|
||||||
* still unclaimed" cannot be read off Dividend Payable. This walks the declarations themselves instead: locks carry the
|
|
||||||
* per-member entitlement and their status says what became of it, which is the authoritative record. The GL is where the
|
|
||||||
* money moved; the locks are what it moved for.
|
|
||||||
*
|
|
||||||
* The columns mirror the money's actual path, which begins and ends in the same place:
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* declare Dr Bank Profit Cr Dividend Payable
|
|
||||||
* member claims Dr Dividend Payable Cr Dividend Paid → Cr Sponsor DD Payable
|
|
||||||
* 3 yrs unclaimed Dr Dividend Payable Cr Bank Profit
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* So for a closed year, claimed + forfeited + rounding residue should account for the whole declaration; anything left
|
|
||||||
* is what is still sitting unclaimed and still within its three-year window.
|
|
||||||
*/
|
|
||||||
@Path("/v1/accounting/dividend-yearwise-report")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Dividend Year-wise Report", description = "Declared, claimed, unclaimed and forfeited dividend per financial year.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DividendYearwiseReportApiResource {
|
|
||||||
|
|
||||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "DIVIDEND_SHAREPRODUCT";
|
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public String report() {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
|
||||||
|
|
||||||
// A declaration is labelled by the financial year its period CLOSES in, which is how a member
|
|
||||||
// and an auditor both refer to it: a period ending 31-Mar-2027 is FY 2026-27. Locks are joined
|
|
||||||
// through the per-member detail rather than counted off the declaration, because a lock is only
|
|
||||||
// created for a member who actually had something to collect.
|
|
||||||
// Declared amounts and lock outcomes are aggregated separately before being joined by year.
|
|
||||||
// Summing the declaration amount across a join to its locks would multiply it by the number
|
|
||||||
// of members it was split between.
|
|
||||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
|
||||||
WITH fy AS (
|
|
||||||
SELECT d.id,
|
|
||||||
CASE WHEN EXTRACT(MONTH FROM d.dividend_period_end_date) >= 4
|
|
||||||
THEN EXTRACT(YEAR FROM d.dividend_period_end_date)
|
|
||||||
ELSE EXTRACT(YEAR FROM d.dividend_period_end_date) - 1 END AS fy_start,
|
|
||||||
d.amount
|
|
||||||
FROM m_share_product_dividend_pay_out d
|
|
||||||
),
|
|
||||||
declared AS (
|
|
||||||
SELECT fy_start, COUNT(*) AS declaration_count, SUM(amount) AS declared_amount
|
|
||||||
FROM fy GROUP BY fy_start
|
|
||||||
),
|
|
||||||
outcomes AS (
|
|
||||||
SELECT fy.fy_start,
|
|
||||||
COALESCE(SUM(CASE WHEN l.status = 'CLAIMED' THEN l.amount ELSE 0 END), 0) AS claimed_amount,
|
|
||||||
COUNT(l.id) FILTER (WHERE l.status = 'CLAIMED') AS claimed_count,
|
|
||||||
COALESCE(SUM(CASE WHEN l.status = 'UNCLAIMED' THEN l.amount ELSE 0 END), 0) AS unclaimed_amount,
|
|
||||||
COUNT(l.id) FILTER (WHERE l.status = 'UNCLAIMED') AS unclaimed_count,
|
|
||||||
COALESCE(SUM(CASE WHEN l.status = 'FORFEITED' THEN l.amount ELSE 0 END), 0) AS forfeited_amount,
|
|
||||||
COUNT(l.id) FILTER (WHERE l.status = 'FORFEITED') AS forfeited_count,
|
|
||||||
MIN(l.release_due_date) FILTER (WHERE l.status = 'UNCLAIMED') AS earliest_release_due
|
|
||||||
FROM fy
|
|
||||||
LEFT JOIN m_share_account_dividend_details sad ON sad.dividend_pay_out_id = fy.id
|
|
||||||
LEFT JOIN m_share_dividend_lock l ON l.dividend_detail_id = sad.id
|
|
||||||
GROUP BY fy.fy_start
|
|
||||||
)
|
|
||||||
SELECT d.fy_start, d.declaration_count, d.declared_amount,
|
|
||||||
COALESCE(o.claimed_amount, 0) AS claimed_amount, COALESCE(o.claimed_count, 0) AS claimed_count,
|
|
||||||
COALESCE(o.unclaimed_amount, 0) AS unclaimed_amount, COALESCE(o.unclaimed_count, 0) AS unclaimed_count,
|
|
||||||
COALESCE(o.forfeited_amount, 0) AS forfeited_amount, COALESCE(o.forfeited_count, 0) AS forfeited_count,
|
|
||||||
o.earliest_release_due
|
|
||||||
FROM declared d
|
|
||||||
LEFT JOIN outcomes o ON o.fy_start = d.fy_start
|
|
||||||
ORDER BY d.fy_start DESC
|
|
||||||
""");
|
|
||||||
|
|
||||||
// Rounding residue is tracked per declaration rather than per lock — it never belonged to a
|
|
||||||
// member, so no lock was ever created for it. Pulled separately and matched by year.
|
|
||||||
final Map<String, BigDecimal> residueByYear = new LinkedHashMap<>();
|
|
||||||
for (final Map<String, Object> row : jdbcTemplate.queryForList("""
|
|
||||||
SELECT CASE WHEN EXTRACT(MONTH FROM period_end_date) >= 4
|
|
||||||
THEN EXTRACT(YEAR FROM period_end_date)
|
|
||||||
ELSE EXTRACT(YEAR FROM period_end_date) - 1 END AS fy_start,
|
|
||||||
COALESCE(SUM(residue_amount), 0) AS residue
|
|
||||||
FROM m_share_dividend_rounding_residue
|
|
||||||
GROUP BY fy_start
|
|
||||||
""")) {
|
|
||||||
residueByYear.put(financialYearLabel(((Number) row.get("fy_start")).intValue()), (BigDecimal) row.get("residue"));
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Map<String, Object>> years = new ArrayList<>();
|
|
||||||
BigDecimal totalDeclared = BigDecimal.ZERO;
|
|
||||||
BigDecimal totalClaimed = BigDecimal.ZERO;
|
|
||||||
BigDecimal totalUnclaimed = BigDecimal.ZERO;
|
|
||||||
BigDecimal totalForfeited = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
for (final Map<String, Object> row : rows) {
|
|
||||||
final String financialYear = financialYearLabel(((Number) row.get("fy_start")).intValue());
|
|
||||||
final BigDecimal declared = (BigDecimal) row.get("declared_amount");
|
|
||||||
final BigDecimal claimed = (BigDecimal) row.get("claimed_amount");
|
|
||||||
final BigDecimal unclaimed = (BigDecimal) row.get("unclaimed_amount");
|
|
||||||
final BigDecimal forfeited = (BigDecimal) row.get("forfeited_amount");
|
|
||||||
final BigDecimal residue = residueByYear.getOrDefault(financialYear, BigDecimal.ZERO);
|
|
||||||
|
|
||||||
final Map<String, Object> entry = new LinkedHashMap<>();
|
|
||||||
entry.put("financialYear", financialYear);
|
|
||||||
entry.put("declarationCount", row.get("declaration_count"));
|
|
||||||
entry.put("declaredAmount", declared);
|
|
||||||
entry.put("claimedAmount", claimed);
|
|
||||||
entry.put("claimedCount", row.get("claimed_count"));
|
|
||||||
entry.put("unclaimedAmount", unclaimed);
|
|
||||||
entry.put("unclaimedCount", row.get("unclaimed_count"));
|
|
||||||
entry.put("forfeitedAmount", forfeited);
|
|
||||||
entry.put("forfeitedCount", row.get("forfeited_count"));
|
|
||||||
entry.put("roundingResidueAmount", residue);
|
|
||||||
// What has already gone back to profit for this year: forfeited entitlements plus the
|
|
||||||
// rounding residue swept at declaration time.
|
|
||||||
entry.put("returnedToProfitAmount", forfeited.add(residue));
|
|
||||||
// When this year's oldest unclaimed lock forfeits — the date the remaining unclaimed
|
|
||||||
// amount starts returning to profit.
|
|
||||||
entry.put("earliestReleaseDueDate",
|
|
||||||
row.get("earliest_release_due") == null ? null : String.valueOf(row.get("earliest_release_due")));
|
|
||||||
years.add(entry);
|
|
||||||
|
|
||||||
totalDeclared = totalDeclared.add(declared);
|
|
||||||
totalClaimed = totalClaimed.add(claimed);
|
|
||||||
totalUnclaimed = totalUnclaimed.add(unclaimed);
|
|
||||||
totalForfeited = totalForfeited.add(forfeited);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, Object> response = new LinkedHashMap<>();
|
|
||||||
response.put("years", years);
|
|
||||||
response.put("totalDeclared", totalDeclared);
|
|
||||||
response.put("totalClaimed", totalClaimed);
|
|
||||||
response.put("totalUnclaimed", totalUnclaimed);
|
|
||||||
response.put("totalForfeited", totalForfeited);
|
|
||||||
return gson.toJson(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String financialYearLabel(final int startYear) {
|
|
||||||
return startYear + "-" + String.format("%02d", (startYear + 1) % 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-58
@@ -1,58 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.accounting.glaccount.validation;
|
|
||||||
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
|
||||||
import org.springframework.dao.EmptyResultDataAccessException;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Header GL account (account_usage = HEADER) is a pure grouping node in the chart-of-accounts tree - it
|
|
||||||
* exists to organize Detail accounts underneath it, never to receive a transaction itself. The standard
|
|
||||||
* Fineract journal-entry command path (JournalEntryWritePlatformServiceJpaRepositoryImpl) never checked
|
|
||||||
* this - only "disabled" and "manual entries allowed" - and several features in this codebase post journal
|
|
||||||
* entries via direct JDBC INSERT into acc_gl_journal_entry, bypassing that command path entirely. This is
|
|
||||||
* the one shared check every such call site uses so a Header account can never end up with its own
|
|
||||||
* transaction (which would silently break every report that assumes a Header's balance is just the sum of
|
|
||||||
* its Detail children).
|
|
||||||
*/
|
|
||||||
public final class GLAccountTransactionGuard {
|
|
||||||
|
|
||||||
private GLAccountTransactionGuard() {}
|
|
||||||
|
|
||||||
public static void assertPostable(final JdbcTemplate jdbcTemplate, final Long glAccountId) {
|
|
||||||
if (glAccountId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final Integer usage;
|
|
||||||
try {
|
|
||||||
usage = jdbcTemplate.queryForObject("SELECT account_usage FROM acc_gl_account WHERE id = ?", Integer.class, glAccountId);
|
|
||||||
} catch (final EmptyResultDataAccessException e) {
|
|
||||||
throw new PlatformDataIntegrityException("error.msg.glaccount.not.found",
|
|
||||||
"GL account with id " + glAccountId + " does not exist.", "glAccountId", glAccountId);
|
|
||||||
}
|
|
||||||
if (GLAccountUsage.HEADER.getValue().equals(usage)) {
|
|
||||||
throw new PlatformDataIntegrityException("error.msg.glaccount.header.account.not.postable",
|
|
||||||
"GL account " + glAccountId + " is a Header account (a chart-of-accounts grouping node) and cannot receive a "
|
|
||||||
+ "transaction directly. Select a Detail account instead.",
|
|
||||||
"glAccountId", glAccountId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1430
-1667
File diff suppressed because it is too large
Load Diff
+3
-6
@@ -170,12 +170,9 @@ public class CashBasedAccountingProcessorForSavings implements AccountingProcess
|
|||||||
// Post journal entry if earned interest amount is greater than
|
// Post journal entry if earned interest amount is greater than
|
||||||
// zero
|
// zero
|
||||||
if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) {
|
if (savingsTransactionDTO.getAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
// Routed through the payable and paid accounts the product names, so the society
|
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
|
||||||
// can read interest declared but not yet credited as its own balance. Collapses
|
CashAccountsForSavings.INTEREST_ON_SAVINGS.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
|
||||||
// to the plain Dr Interest On Savings / Cr Savings Control pair for a product
|
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
|
||||||
// that names neither.
|
|
||||||
this.helper.createInterestPostingJournalEntriesForSavings(office, currencyCode, savingsProductId, paymentTypeId,
|
|
||||||
savingsId, transactionId, transactionDate, amount, isReversal);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user